zcash_client_backend: Make it possible for change strategies to use wallet metadata.

In the process this modifies input selection to take the change strategy
as an explicit argument, rather than being wrapped as part of the input
selector.
This commit is contained in:
Kris Nuttycombe 2024-10-07 14:58:50 -06:00
parent e21fce4411
commit 6d5a6ac7ac
16 changed files with 746 additions and 486 deletions

View File

@ -12,10 +12,55 @@ and this library adheres to Rust's notion of
- `Progress`
- `WalletSummary::progress`
- `WalletMeta`
- `impl Default for wallet::input_selection::GreedyInputSelector`
### Changed
- `zcash_client_backend::data_api`:
- `InputSource` has an added method `get_wallet_metadata`
- `error::Error` has additional variant `Error::Change`. This necessitates
the addition of two type parameters to the `Error` type,
`ChangeErrT` and `NoteRefT`.
- The following methods each now take an additional `change_strategy`
argument, along with an associated `ChangeT` type parameter:
- `zcash_client_backend::data_api::wallet::spend`
- `zcash_client_backend::data_api::wallet::propose_transfer`
- `zcash_client_backend::data_api::wallet::propose_shielding`. This method
also now takes an additional `to_account` argument.
- `zcash_client_backend::data_api::wallet::shield_transparent_funds`. This
method also now takes an additional `to_account` argument.
- `wallet::input_selection::InputSelectionError` now has an additional `Change`
variant. This necessitates the addition of two type parameters.
- `wallet::input_selection::InputSelector::propose_transaction` takes an
additional `change_strategy` argument, along with an associated `ChangeT`
type parameter.
- The `wallet::input_selection::InputSelector::FeeRule` associated type has
been removed. The fee rule is now part of the change strategy passed to
`propose_transaction`.
- `wallet::input_selection::ShieldingSelector::propose_shielding` takes an
additional `change_strategy` argument, along with an associated `ChangeT`
type parameter. In addition, it also takes a new `to_account` argument
that identifies the destination account for the shielded notes.
- The `wallet::input_selection::ShieldingSelector::FeeRule` associated type
has been removed. The fee rule is now part of the change strategy passed to
`propose_shielding`.
- The `Change` variant of `wallet::input_selection::GreedyInputSelectorError`
has been removed, along with the additional type parameters it necessitated.
- The arguments to `wallet::input_selection::GreedyInputSelector::new` have
changed.
- `zcash_client_backend::fees::ChangeStrategy` has changed. It has two new
associated types, `MetaSource` and `WalletMeta`, and its `FeeRule` associated
type now has an additional `Clone` bound. In addition, it defines a new
`fetch_wallet_meta` method, and the arguments to `compute_balance` have
changed.
- `zcash_client_backend::fees::fixed::SingleOutputChangeStrategy::new`
now takes an additional `DustOutputPolicy` argument. It also now carries
an additional type parameter.
- `zcash_client_backend::fees::standard::SingleOutputChangeStrategy::new`
now takes an additional `DustOutputPolicy` argument. It also now carries
an additional type parameter.
- `zcash_client_backend::fees::zip317::SingleOutputChangeStrategy::new`
now takes an additional `DustOutputPolicy` argument. It also now carries
an additional type parameter.
### Changed
- MSRV is now 1.77.0.
@ -25,6 +70,8 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::data_api`:
- `WalletSummary::scan_progress` and `WalletSummary::recovery_progress` have
been removed. Use `WalletSummary::progress` instead.
- `zcash_client_backend::fees`:
- `impl From<BalanceError> for ChangeError<...>`
## [0.14.0] - 2024-10-04

View File

@ -794,7 +794,7 @@ impl<NoteRef> SpendableNotes<NoteRef> {
}
}
/// Metadata about the structure of the wallet for a particular account.
/// Metadata about the structure of the wallet for a particular account.
///
/// At present this just contains counts of unspent outputs in each pool, but it may be extended in
/// the future to contain note values or other more detailed information about wallet structure.

View File

@ -13,6 +13,7 @@ use zcash_primitives::transaction::{
use crate::address::UnifiedAddress;
use crate::data_api::wallet::input_selection::InputSelectorError;
use crate::fees::ChangeError;
use crate::proposal::ProposalError;
use crate::PoolType;
@ -23,7 +24,8 @@ use crate::wallet::NoteId;
/// Errors that can occur as a consequence of wallet operations.
#[derive(Debug)]
pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError, ChangeErrT, NoteRefT>
{
/// An error occurred retrieving data from the underlying data source
DataSource(DataSourceError),
@ -33,6 +35,9 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
/// An error in note selection
NoteSelection(SelectionError),
/// An error in change selection during transaction proposal construction
Change(ChangeError<ChangeErrT, NoteRefT>),
/// An error in transaction proposal construction
Proposal(ProposalError),
@ -98,12 +103,14 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
PaysEphemeralTransparentAddress(String),
}
impl<DE, CE, SE, FE> fmt::Display for Error<DE, CE, SE, FE>
impl<DE, TE, SE, FE, CE, N> fmt::Display for Error<DE, TE, SE, FE, CE, N>
where
DE: fmt::Display,
CE: fmt::Display,
TE: fmt::Display,
SE: fmt::Display,
FE: fmt::Display,
CE: fmt::Display,
N: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use fmt::Write;
@ -122,6 +129,9 @@ where
Error::NoteSelection(e) => {
write!(f, "Note selection encountered the following error: {}", e)
}
Error::Change(e) => {
write!(f, "Change output generation failed: {}", e)
}
Error::Proposal(e) => {
write!(f, "Input selection attempted to construct an invalid proposal: {}", e)
}
@ -178,12 +188,14 @@ where
}
}
impl<DE, CE, SE, FE> error::Error for Error<DE, CE, SE, FE>
impl<DE, TE, SE, FE, CE, N> error::Error for Error<DE, TE, SE, FE, CE, N>
where
DE: Debug + Display + error::Error + 'static,
CE: Debug + Display + error::Error + 'static,
TE: Debug + Display + error::Error + 'static,
SE: Debug + Display + error::Error + 'static,
FE: Debug + Display + 'static,
CE: Debug + Display + error::Error + 'static,
N: Debug + Display + 'static,
{
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self {
@ -197,35 +209,38 @@ where
}
}
impl<DE, CE, SE, FE> From<builder::Error<FE>> for Error<DE, CE, SE, FE> {
impl<DE, TE, SE, FE, CE, N> From<builder::Error<FE>> for Error<DE, TE, SE, FE, CE, N> {
fn from(e: builder::Error<FE>) -> Self {
Error::Builder(e)
}
}
impl<DE, CE, SE, FE> From<ProposalError> for Error<DE, CE, SE, FE> {
impl<DE, TE, SE, FE, CE, N> From<ProposalError> for Error<DE, TE, SE, FE, CE, N> {
fn from(e: ProposalError) -> Self {
Error::Proposal(e)
}
}
impl<DE, CE, SE, FE> From<BalanceError> for Error<DE, CE, SE, FE> {
impl<DE, TE, SE, FE, CE, N> From<BalanceError> for Error<DE, TE, SE, FE, CE, N> {
fn from(e: BalanceError) -> Self {
Error::BalanceError(e)
}
}
impl<DE, CE, SE, FE> From<ConversionError<&'static str>> for Error<DE, CE, SE, FE> {
impl<DE, TE, SE, FE, CE, N> From<ConversionError<&'static str>> for Error<DE, TE, SE, FE, CE, N> {
fn from(value: ConversionError<&'static str>) -> Self {
Error::Address(value)
}
}
impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE> {
fn from(e: InputSelectorError<DE, SE>) -> Self {
impl<DE, TE, SE, FE, CE, N> From<InputSelectorError<DE, SE, CE, N>>
for Error<DE, TE, SE, FE, CE, N>
{
fn from(e: InputSelectorError<DE, SE, CE, N>) -> Self {
match e {
InputSelectorError::DataSource(e) => Error::DataSource(e),
InputSelectorError::Selection(e) => Error::NoteSelection(e),
InputSelectorError::Change(e) => Error::Change(e),
InputSelectorError::Proposal(e) => Error::Proposal(e),
InputSelectorError::InsufficientFunds {
available,
@ -240,20 +255,20 @@ impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE>
}
}
impl<DE, CE, SE, FE> From<sapling::builder::Error> for Error<DE, CE, SE, FE> {
impl<DE, TE, SE, FE, CE, N> From<sapling::builder::Error> for Error<DE, TE, SE, FE, CE, N> {
fn from(e: sapling::builder::Error) -> Self {
Error::Builder(builder::Error::SaplingBuild(e))
}
}
impl<DE, CE, SE, FE> From<transparent::builder::Error> for Error<DE, CE, SE, FE> {
impl<DE, TE, SE, FE, CE, N> From<transparent::builder::Error> for Error<DE, TE, SE, FE, CE, N> {
fn from(e: transparent::builder::Error) -> Self {
Error::Builder(builder::Error::TransparentBuild(e))
}
}
impl<DE, CE, SE, FE> From<ShardTreeError<CE>> for Error<DE, CE, SE, FE> {
fn from(e: ShardTreeError<CE>) -> Self {
impl<DE, TE, SE, FE, CE, N> From<ShardTreeError<TE>> for Error<DE, TE, SE, FE, CE, N> {
fn from(e: ShardTreeError<TE>) -> Self {
Error::CommitmentTree(e)
}
}

View File

@ -31,7 +31,7 @@ use zcash_primitives::{
memo::Memo,
transaction::{
components::{amount::NonNegativeAmount, sapling::zip212_enforcement},
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule},
fees::{FeeRule, StandardFeeRule},
Transaction, TxId,
},
};
@ -46,7 +46,10 @@ use zip32::{fingerprint::SeedFingerprint, DiversifierIndex};
use crate::{
address::UnifiedAddress,
fees::{standard, DustOutputPolicy},
fees::{
standard::{self, SingleOutputChangeStrategy},
ChangeStrategy, DustOutputPolicy,
},
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
proposal::Proposal,
proto::compact_formats::{
@ -62,7 +65,7 @@ use super::{
scanning::ScanRange,
wallet::{
create_proposed_transactions, create_spend_to_address,
input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector},
input_selection::{GreedyInputSelector, InputSelector},
propose_standard_transfer_to_address, propose_transfer, spend,
},
Account, AccountBalance, AccountBirthday, AccountPurpose, AccountSource, BlockMetadata,
@ -874,12 +877,7 @@ where
fallback_change_pool: ShieldedProtocol,
) -> Result<
NonEmpty<TxId>,
super::error::Error<
ErrT,
<DbT as WalletCommitmentTrees>::Error,
GreedyInputSelectorError<Zip317FeeError, <DbT as InputSource>::NoteRef>,
Zip317FeeError,
>,
super::wallet::TransferErrT<DbT, GreedyInputSelector<DbT>, SingleOutputChangeStrategy<DbT>>,
> {
let prover = LocalTxProver::bundled();
let network = self.network().clone();
@ -901,24 +899,18 @@ where
/// Invokes [`spend`] with the given arguments.
#[allow(clippy::type_complexity)]
pub fn spend<InputsT>(
pub fn spend<InputsT, ChangeT>(
&mut self,
input_selector: &InputsT,
change_strategy: &ChangeT,
usk: &UnifiedSpendingKey,
request: zip321::TransactionRequest,
ovk_policy: OvkPolicy,
min_confirmations: NonZeroU32,
) -> Result<
NonEmpty<TxId>,
super::error::Error<
ErrT,
<DbT as WalletCommitmentTrees>::Error,
InputsT::Error,
<InputsT::FeeRule as FeeRule>::Error,
>,
>
) -> Result<NonEmpty<TxId>, super::wallet::TransferErrT<DbT, InputsT, ChangeT>>
where
InputsT: InputSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
#![allow(deprecated)]
let prover = LocalTxProver::bundled();
@ -929,6 +921,7 @@ where
&prover,
&prover,
input_selector,
change_strategy,
usk,
request,
ovk_policy,
@ -938,25 +931,28 @@ where
/// Invokes [`propose_transfer`] with the given arguments.
#[allow(clippy::type_complexity)]
pub fn propose_transfer<InputsT>(
pub fn propose_transfer<InputsT, ChangeT>(
&mut self,
spend_from_account: <DbT as InputSource>::AccountId,
input_selector: &InputsT,
change_strategy: &ChangeT,
request: zip321::TransactionRequest,
min_confirmations: NonZeroU32,
) -> Result<
Proposal<InputsT::FeeRule, <DbT as InputSource>::NoteRef>,
super::error::Error<ErrT, Infallible, InputsT::Error, <InputsT::FeeRule as FeeRule>::Error>,
Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
super::wallet::ProposeTransferErrT<DbT, Infallible, InputsT, ChangeT>,
>
where
InputsT: InputSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let network = self.network().clone();
propose_transfer::<_, _, _, Infallible>(
propose_transfer::<_, _, _, _, Infallible>(
self.wallet_mut(),
&network,
spend_from_account,
input_selector,
change_strategy,
request,
min_confirmations,
)
@ -977,11 +973,11 @@ where
fallback_change_pool: ShieldedProtocol,
) -> Result<
Proposal<StandardFeeRule, <DbT as InputSource>::NoteRef>,
super::error::Error<
ErrT,
super::wallet::ProposeTransferErrT<
DbT,
CommitmentTreeErrT,
GreedyInputSelectorError<Zip317FeeError, <DbT as InputSource>::NoteRef>,
Zip317FeeError,
GreedyInputSelector<DbT>,
SingleOutputChangeStrategy<DbT>,
>,
> {
let network = self.network().clone();
@ -1011,47 +1007,47 @@ where
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::type_complexity)]
#[allow(dead_code)]
pub fn propose_shielding<InputsT>(
pub fn propose_shielding<InputsT, ChangeT>(
&mut self,
input_selector: &InputsT,
change_strategy: &ChangeT,
shielding_threshold: NonNegativeAmount,
from_addrs: &[TransparentAddress],
to_account: <InputsT::InputSource as InputSource>::AccountId,
min_confirmations: u32,
) -> Result<
Proposal<InputsT::FeeRule, Infallible>,
super::error::Error<ErrT, Infallible, InputsT::Error, <InputsT::FeeRule as FeeRule>::Error>,
Proposal<ChangeT::FeeRule, Infallible>,
super::wallet::ProposeShieldingErrT<DbT, Infallible, InputsT, ChangeT>,
>
where
InputsT: ShieldingSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
use super::wallet::propose_shielding;
let network = self.network().clone();
propose_shielding::<_, _, _, Infallible>(
propose_shielding::<_, _, _, _, Infallible>(
self.wallet_mut(),
&network,
input_selector,
change_strategy,
shielding_threshold,
from_addrs,
to_account,
min_confirmations,
)
}
/// Invokes [`create_proposed_transactions`] with the given arguments.
#[allow(clippy::type_complexity)]
pub fn create_proposed_transactions<InputsErrT, FeeRuleT>(
pub fn create_proposed_transactions<InputsErrT, FeeRuleT, ChangeErrT>(
&mut self,
usk: &UnifiedSpendingKey,
ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
) -> Result<
NonEmpty<TxId>,
super::error::Error<
ErrT,
<DbT as WalletCommitmentTrees>::Error,
InputsErrT,
FeeRuleT::Error,
>,
super::wallet::CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, DbT::NoteRef>,
>
where
FeeRuleT: FeeRule,
@ -1074,24 +1070,20 @@ where
/// [`shield_transparent_funds`]: crate::data_api::wallet::shield_transparent_funds
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::type_complexity)]
pub fn shield_transparent_funds<InputsT>(
#[allow(clippy::too_many_arguments)]
pub fn shield_transparent_funds<InputsT, ChangeT>(
&mut self,
input_selector: &InputsT,
change_strategy: &ChangeT,
shielding_threshold: NonNegativeAmount,
usk: &UnifiedSpendingKey,
from_addrs: &[TransparentAddress],
to_account: <DbT as InputSource>::AccountId,
min_confirmations: u32,
) -> Result<
NonEmpty<TxId>,
super::error::Error<
ErrT,
<DbT as WalletCommitmentTrees>::Error,
InputsT::Error,
<InputsT::FeeRule as FeeRule>::Error,
>,
>
) -> Result<NonEmpty<TxId>, super::wallet::ShieldErrT<DbT, InputsT, ChangeT>>
where
InputsT: ShieldingSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
use crate::data_api::wallet::shield_transparent_funds;
@ -1103,9 +1095,11 @@ where
&prover,
&prover,
input_selector,
change_strategy,
shielding_threshold,
usk,
from_addrs,
to_account,
min_confirmations,
)
}
@ -1229,15 +1223,22 @@ impl<Cache, DbT: WalletRead + Reset> TestState<Cache, DbT, LocalNetwork> {
/// Helper method for constructing a [`GreedyInputSelector`] with a
/// [`standard::SingleOutputChangeStrategy`].
pub fn input_selector<DbT: InputSource>(
pub fn input_selector<DbT: InputSource>() -> GreedyInputSelector<DbT> {
GreedyInputSelector::<DbT>::new()
}
pub fn single_output_change_strategy<DbT: InputSource>(
fee_rule: StandardFeeRule,
change_memo: Option<&str>,
fallback_change_pool: ShieldedProtocol,
) -> GreedyInputSelector<DbT, standard::SingleOutputChangeStrategy> {
) -> standard::SingleOutputChangeStrategy<DbT> {
let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap()));
let change_strategy =
standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, fallback_change_pool);
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default())
standard::SingleOutputChangeStrategy::new(
fee_rule,
change_memo,
fallback_change_pool,
DustOutputPolicy::default(),
)
}
// Checks that a protobuf proposal serialized from the provided proposal value correctly parses to

View File

@ -17,9 +17,7 @@ use zcash_primitives::{
legacy::TransparentAddress,
transaction::{
components::amount::NonNegativeAmount,
fees::{
fixed::FeeRule as FixedFeeRule, zip317::FeeError as Zip317FeeError, StandardFeeRule,
},
fees::{fixed::FeeRule as FixedFeeRule, StandardFeeRule},
Transaction,
},
};
@ -38,16 +36,22 @@ use crate::{
self,
chain::{self, ChainState, CommitmentTreeRoot, ScanSummary},
error::Error,
testing::{input_selector, AddressType, FakeCompactOutput, InitialChainState, TestBuilder},
testing::{
single_output_change_strategy, AddressType, FakeCompactOutput, InitialChainState,
TestBuilder,
},
wallet::{
decrypt_and_store_transaction,
input_selection::{GreedyInputSelector, GreedyInputSelectorError},
decrypt_and_store_transaction, input_selection::GreedyInputSelector, TransferErrT,
},
Account as _, AccountBirthday, DecryptedTransaction, InputSource, Ratio,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletTest, WalletWrite,
},
decrypt_transaction,
fees::{fixed, standard, DustOutputPolicy},
fees::{
fixed,
standard::{self, SingleOutputChangeStrategy},
DustOutputPolicy,
},
scanning::ScanError,
wallet::{Note, NoteId, OvkPolicy, ReceivedNote},
};
@ -216,19 +220,21 @@ pub fn send_single_step_proposed_transfer<T: ShieldedPoolTester>(
fee_rule,
Some(change_memo.clone().into()),
T::SHIELDED_PROTOCOL,
DustOutputPolicy::default(),
);
let input_selector = &GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
let input_selector = GreedyInputSelector::new();
let proposal = st
.propose_transfer(
account.id(),
input_selector,
&input_selector,
&change_strategy,
request,
NonZeroU32::new(1).unwrap(),
)
.unwrap();
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal,
@ -399,7 +405,7 @@ pub fn send_multi_step_proposed_transfer<T: ShieldedPoolTester, DSF>(
);
assert_eq!(steps[1].balance().proposed_change(), []);
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal,
@ -499,7 +505,7 @@ pub fn send_multi_step_proposed_transfer<T: ShieldedPoolTester, DSF>(
)
.unwrap();
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal,
@ -758,7 +764,7 @@ pub fn proposal_fails_if_not_all_ephemeral_outputs_consumed<T: ShieldedPoolTeste
// This is somewhat redundant with `send_multi_step_proposed_transfer`,
// but tests the case with no change memo and ensures we haven't messed
// up the test setup.
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal,
@ -774,7 +780,7 @@ pub fn proposal_fails_if_not_all_ephemeral_outputs_consumed<T: ShieldedPoolTeste
)
.unwrap();
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&frobbed_proposal,
@ -998,7 +1004,11 @@ pub fn spend_fails_on_unverified_notes<T: ShieldedPoolTester>(
// Executing the proposal should succeed
let txid = st
.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal)
.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal,
)
.unwrap()[0];
let (h, _) = st.generate_next_block_including(txid);
@ -1057,7 +1067,7 @@ pub fn spend_fails_on_locked_notes<T: ShieldedPoolTester>(
// Executing the proposal should succeed
assert_matches!(
st.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal,),
st.create_proposed_transactions::<Infallible, _, Infallible>(account.usk(), OvkPolicy::Sender, &proposal,),
Ok(txids) if txids.len() == 1
);
@ -1139,7 +1149,11 @@ pub fn spend_fails_on_locked_notes<T: ShieldedPoolTester>(
.unwrap();
let txid2 = st
.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal)
.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal,
)
.unwrap()[0];
let (h, _) = st.generate_next_block_including(txid2);
@ -1187,7 +1201,11 @@ pub fn ovk_policy_prevents_recovery_from_chain<T: ShieldedPoolTester, DSF>(
ovk_policy|
-> Result<
Option<(Note, Address, MemoBytes)>,
Error<_, _, GreedyInputSelectorError<Zip317FeeError, _>, Zip317FeeError>,
TransferErrT<
DSF::DataStore,
GreedyInputSelector<DSF::DataStore>,
SingleOutputChangeStrategy<DSF::DataStore>,
>,
> {
let min_confirmations = NonZeroU32::new(1).unwrap();
let proposal = st.propose_standard_transfer(
@ -1283,7 +1301,7 @@ pub fn spend_succeeds_to_t_addr_zero_change<T: ShieldedPoolTester>(
// Executing the proposal should succeed
assert_matches!(
st.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal),
st.create_proposed_transactions::<Infallible, _, Infallible>(account.usk(), OvkPolicy::Sender, &proposal),
Ok(txids) if txids.len() == 1
);
}
@ -1346,7 +1364,7 @@ pub fn change_note_spends_succeed<T: ShieldedPoolTester>(
// Executing the proposal should succeed
assert_matches!(
st.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal),
st.create_proposed_transactions::<Infallible, _, Infallible>(account.usk(), OvkPolicy::Sender, &proposal),
Ok(txids) if txids.len() == 1
);
}
@ -1396,14 +1414,18 @@ pub fn external_address_change_spends_detected_in_restore_from_seed<T: ShieldedP
#[allow(deprecated)]
let fee_rule = FixedFeeRule::standard();
let input_selector = GreedyInputSelector::new(
fixed::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL),
let change_strategy = fixed::SingleOutputChangeStrategy::new(
fee_rule,
None,
T::SHIELDED_PROTOCOL,
DustOutputPolicy::default(),
);
let input_selector = GreedyInputSelector::new();
let txid = st
.spend(
&input_selector,
&change_strategy,
&usk,
req,
OvkPolicy::Sender,
@ -1447,8 +1469,8 @@ pub fn external_address_change_spends_detected_in_restore_from_seed<T: ShieldedP
}
#[allow(dead_code)]
pub fn zip317_spend<T: ShieldedPoolTester>(
ds_factory: impl DataStoreFactory,
pub fn zip317_spend<T: ShieldedPoolTester, DSF: DataStoreFactory>(
ds_factory: DSF,
cache: impl TestCache,
) {
let mut st = TestBuilder::new()
@ -1484,7 +1506,9 @@ pub fn zip317_spend<T: ShieldedPoolTester>(
assert_eq!(st.get_total_balance(account_id), total);
assert_eq!(st.get_spendable_balance(account_id, 1), total);
let input_selector = input_selector(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
let input_selector = GreedyInputSelector::<DSF::DataStore>::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
// This first request will fail due to insufficient non-dust funds
let req = TransactionRequest::new(vec![Payment::without_memo(
@ -1496,6 +1520,7 @@ pub fn zip317_spend<T: ShieldedPoolTester>(
assert_matches!(
st.spend(
&input_selector,
&change_strategy,
account.usk(),
req,
OvkPolicy::Sender,
@ -1517,6 +1542,7 @@ pub fn zip317_spend<T: ShieldedPoolTester>(
let txid = st
.spend(
&input_selector,
&change_strategy,
account.usk(),
req,
OvkPolicy::Sender,
@ -1579,19 +1605,18 @@ where
let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo);
assert_matches!(res0, Ok(_));
let fee_rule = StandardFeeRule::Zip317;
let input_selector = GreedyInputSelector::new(
standard::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL),
DustOutputPolicy::default(),
);
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
let txids = st
.shield_transparent_funds(
&input_selector,
&change_strategy,
NonNegativeAmount::from_u64(10000).unwrap(),
account.usk(),
&[*taddr],
account.id(),
1,
)
.unwrap();
@ -1827,15 +1852,14 @@ pub fn pool_crossing_required<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
)])
.unwrap();
let fee_rule = StandardFeeRule::Zip317;
let input_selector = GreedyInputSelector::new(
standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL),
DustOutputPolicy::default(),
);
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL);
let proposal0 = st
.propose_transfer(
account.id(),
&input_selector,
&change_strategy,
p0_to_p1,
NonZeroU32::new(1).unwrap(),
)
@ -1863,7 +1887,7 @@ pub fn pool_crossing_required<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
);
assert_eq!(change_output.value(), expected_change);
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal0,
@ -1918,17 +1942,16 @@ pub fn fully_funded_fully_private<P0: ShieldedPoolTester, P1: ShieldedPoolTester
)])
.unwrap();
let fee_rule = StandardFeeRule::Zip317;
let input_selector = GreedyInputSelector::new(
// We set the default change output pool to P0, because we want to verify later that
// change is actually sent to P1 (as the transaction is fully fundable from P1).
standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL),
DustOutputPolicy::default(),
);
let input_selector = GreedyInputSelector::new();
// We set the default change output pool to P0, because we want to verify later that
// change is actually sent to P1 (as the transaction is fully fundable from P1).
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL);
let proposal0 = st
.propose_transfer(
account.id(),
&input_selector,
&change_strategy,
p0_to_p1,
NonZeroU32::new(1).unwrap(),
)
@ -1955,7 +1978,7 @@ pub fn fully_funded_fully_private<P0: ShieldedPoolTester, P1: ShieldedPoolTester
);
assert_eq!(change_output.value(), expected_change);
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal0,
@ -2009,17 +2032,16 @@ pub fn fully_funded_send_to_t<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
)])
.unwrap();
let fee_rule = StandardFeeRule::Zip317;
let input_selector = GreedyInputSelector::new(
// We set the default change output pool to P0, because we want to verify later that
// change is actually sent to P1 (as the transaction is fully fundable from P1).
standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL),
DustOutputPolicy::default(),
);
let input_selector = GreedyInputSelector::new();
// We set the default change output pool to P0, because we want to verify later that
// change is actually sent to P1 (as the transaction is fully fundable from P1).
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL);
let proposal0 = st
.propose_transfer(
account.id(),
&input_selector,
&change_strategy,
p0_to_p1,
NonZeroU32::new(1).unwrap(),
)
@ -2043,7 +2065,7 @@ pub fn fully_funded_send_to_t<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
assert_eq!(change_output.output_pool(), PoolType::SAPLING);
assert_eq!(change_output.value(), expected_change);
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal0,
@ -2110,11 +2132,9 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
assert_eq!(st.get_spendable_balance(acct_id, 1), initial_balance);
// Set up the fee rule and input selector we'll use for all the transfers.
let fee_rule = StandardFeeRule::Zip317;
let input_selector = GreedyInputSelector::new(
standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL),
DustOutputPolicy::default(),
);
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL);
// First, send funds just to P0
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
@ -2126,6 +2146,7 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
let res = st
.spend(
&input_selector,
&change_strategy,
account.usk(),
p0_transfer,
OvkPolicy::Sender,
@ -2157,6 +2178,7 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
let res = st
.spend(
&input_selector,
&change_strategy,
account.usk(),
both_transfer,
OvkPolicy::Sender,
@ -2597,17 +2619,14 @@ pub fn scan_cached_blocks_allows_blocks_out_of_order<T: ShieldedPoolTester>(
.unwrap();
#[allow(deprecated)]
let input_selector = GreedyInputSelector::new(
standard::SingleOutputChangeStrategy::new(
StandardFeeRule::Zip317,
None,
T::SHIELDED_PROTOCOL,
),
DustOutputPolicy::default(),
);
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
assert_matches!(
st.spend(
&input_selector,
&change_strategy,
account.usk(),
req,
OvkPolicy::Sender,

View File

@ -197,16 +197,23 @@ where
check_balance(&st, 0, value);
// Shield the output.
let input_selector = GreedyInputSelector::new(
fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::ZERO),
None,
ShieldedProtocol::Sapling,
),
let input_selector = GreedyInputSelector::new();
let change_strategy = fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::ZERO),
None,
ShieldedProtocol::Sapling,
DustOutputPolicy::default(),
);
let txid = st
.shield_transparent_funds(&input_selector, value, account.usk(), &[*taddr], 1)
.shield_transparent_funds(
&input_selector,
&change_strategy,
value,
account.usk(),
&[*taddr],
account.id(),
1,
)
.unwrap()[0];
// The wallet should have zero transparent balance, because the shielding

View File

@ -50,7 +50,7 @@ use crate::{
WalletRead, WalletWrite,
},
decrypt_transaction,
fees::{self, DustOutputPolicy},
fees::{standard::SingleOutputChangeStrategy, ChangeStrategy, DustOutputPolicy},
keys::UnifiedSpendingKey,
proposal::{Proposal, ProposalError, Step, StepOutputIndex},
wallet::{Note, OvkPolicy, Recipient},
@ -62,7 +62,7 @@ use zcash_primitives::{
transaction::{
builder::{BuildConfig, BuildResult, Builder},
components::{amount::NonNegativeAmount, sapling::zip212_enforcement, OutPoint},
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule},
fees::{FeeRule, StandardFeeRule},
Transaction, TxId,
},
};
@ -83,9 +83,7 @@ use {
};
pub mod input_selection;
use input_selection::{
GreedyInputSelector, GreedyInputSelectorError, InputSelector, InputSelectorError,
};
use input_selection::{GreedyInputSelector, InputSelector, InputSelectorError};
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
/// the wallet, and saves it to the wallet.
@ -117,6 +115,53 @@ where
Ok(())
}
pub type ProposeTransferErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT> = Error<
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
<InputsT as InputSelector>::Error,
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
<ChangeT as ChangeStrategy>::Error,
<<InputsT as InputSelector>::InputSource as InputSource>::NoteRef,
>;
#[cfg(feature = "transparent-inputs")]
pub type ProposeShieldingErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT> = Error<
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
<InputsT as ShieldingSelector>::Error,
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
<ChangeT as ChangeStrategy>::Error,
Infallible,
>;
pub type CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N> = Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
InputsErrT,
<FeeRuleT as FeeRule>::Error,
ChangeErrT,
N,
>;
pub type TransferErrT<DbT, InputsT, ChangeT> = Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
<InputsT as InputSelector>::Error,
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
<ChangeT as ChangeStrategy>::Error,
<<InputsT as InputSelector>::InputSource as InputSource>::NoteRef,
>;
#[cfg(feature = "transparent-inputs")]
pub type ShieldErrT<DbT, InputsT, ChangeT> = Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
<InputsT as ShieldingSelector>::Error,
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
<ChangeT as ChangeStrategy>::Error,
Infallible,
>;
#[allow(clippy::needless_doctest_main)]
/// Creates a transaction or series of transactions paying the specified address from
/// the given account, and the [`TxId`] corresponding to each newly-created transaction.
@ -251,12 +296,7 @@ pub fn create_spend_to_address<DbT, ParamsT>(
fallback_change_pool: ShieldedProtocol,
) -> Result<
NonEmpty<TxId>,
Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
GreedyInputSelectorError<Zip317FeeError, DbT::NoteRef>,
Zip317FeeError,
>,
TransferErrT<DbT, GreedyInputSelector<DbT>, SingleOutputChangeStrategy<DbT>>,
>
where
ParamsT: consensus::Parameters + Clone,
@ -297,13 +337,6 @@ where
)
}
type ErrorT<DbT, InputsErrT, FeeRuleT> = Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
InputsErrT,
<FeeRuleT as FeeRule>::Error,
>;
/// Constructs a transaction or series of transactions that send funds as specified
/// by the `request` argument, stores them to the wallet's "sent transactions" data
/// store, and returns the [`TxId`] for each transaction constructed.
@ -358,17 +391,18 @@ type ErrorT<DbT, InputsErrT, FeeRuleT> = Error<
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
#[deprecated(note = "Use `propose_transfer` and `create_proposed_transactions` instead.")]
pub fn spend<DbT, ParamsT, InputsT>(
pub fn spend<DbT, ParamsT, InputsT, ChangeT>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
input_selector: &InputsT,
change_strategy: &ChangeT,
usk: &UnifiedSpendingKey,
request: zip321::TransactionRequest,
ovk_policy: OvkPolicy,
min_confirmations: NonZeroU32,
) -> Result<NonEmpty<TxId>, ErrorT<DbT, InputsT::Error, InputsT::FeeRule>>
) -> Result<NonEmpty<TxId>, TransferErrT<DbT, InputsT, ChangeT>>
where
DbT: InputSource,
DbT: WalletWrite<
@ -378,6 +412,7 @@ where
DbT: WalletCommitmentTrees,
ParamsT: consensus::Parameters + Clone,
InputsT: InputSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let account = wallet_db
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
@ -389,6 +424,7 @@ where
params,
account.id(),
input_selector,
change_strategy,
request,
min_confirmations,
)?;
@ -409,27 +445,24 @@ where
/// [`create_proposed_transactions`].
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn propose_transfer<DbT, ParamsT, InputsT, CommitmentTreeErrT>(
pub fn propose_transfer<DbT, ParamsT, InputsT, ChangeT, CommitmentTreeErrT>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_from_account: <DbT as InputSource>::AccountId,
input_selector: &InputsT,
change_strategy: &ChangeT,
request: zip321::TransactionRequest,
min_confirmations: NonZeroU32,
) -> Result<
Proposal<InputsT::FeeRule, <DbT as InputSource>::NoteRef>,
Error<
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
InputsT::Error,
<InputsT::FeeRule as FeeRule>::Error,
>,
Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
ProposeTransferErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT>,
>
where
DbT: WalletRead + InputSource<Error = <DbT as WalletRead>::Error>,
<DbT as InputSource>::NoteRef: Copy + Eq + Ord,
ParamsT: consensus::Parameters + Clone,
InputsT: InputSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let (target_height, anchor_height) = wallet_db
.get_target_and_anchor_heights(min_confirmations)
@ -444,6 +477,7 @@ where
anchor_height,
spend_from_account,
request,
change_strategy,
)
.map_err(Error::from)
}
@ -488,11 +522,11 @@ pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
fallback_change_pool: ShieldedProtocol,
) -> Result<
Proposal<StandardFeeRule, DbT::NoteRef>,
Error<
<DbT as WalletRead>::Error,
ProposeTransferErrT<
DbT,
CommitmentTreeErrT,
GreedyInputSelectorError<Zip317FeeError, DbT::NoteRef>,
Zip317FeeError,
GreedyInputSelector<DbT>,
SingleOutputChangeStrategy<DbT>,
>,
>
where
@ -517,19 +551,20 @@ where
"It should not be possible for this to violate ZIP 321 request construction invariants.",
);
let change_strategy = fees::standard::SingleOutputChangeStrategy::new(
let input_selector = GreedyInputSelector::<DbT>::new();
let change_strategy = SingleOutputChangeStrategy::<DbT>::new(
fee_rule,
change_memo,
fallback_change_pool,
DustOutputPolicy::default(),
);
let input_selector =
GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default());
propose_transfer(
wallet_db,
params,
spend_from_account,
&input_selector,
&change_strategy,
request,
min_confirmations,
)
@ -540,26 +575,24 @@ where
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn propose_shielding<DbT, ParamsT, InputsT, CommitmentTreeErrT>(
pub fn propose_shielding<DbT, ParamsT, InputsT, ChangeT, CommitmentTreeErrT>(
wallet_db: &mut DbT,
params: &ParamsT,
input_selector: &InputsT,
change_strategy: &ChangeT,
shielding_threshold: NonNegativeAmount,
from_addrs: &[TransparentAddress],
to_account: <DbT as InputSource>::AccountId,
min_confirmations: u32,
) -> Result<
Proposal<InputsT::FeeRule, Infallible>,
Error<
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
InputsT::Error,
<InputsT::FeeRule as FeeRule>::Error,
>,
Proposal<ChangeT::FeeRule, Infallible>,
ProposeShieldingErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT>,
>
where
ParamsT: consensus::Parameters,
DbT: WalletRead + InputSource<Error = <DbT as WalletRead>::Error>,
InputsT: ShieldingSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let chain_tip_height = wallet_db
.chain_height()
@ -570,8 +603,10 @@ where
.propose_shielding(
params,
wallet_db,
change_strategy,
shielding_threshold,
from_addrs,
to_account,
chain_tip_height + 1,
min_confirmations,
)
@ -599,7 +634,7 @@ struct StepResult<AccountId> {
/// and therefore the required spend proofs for such notes cannot be constructed.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_prover: &impl SpendProver,
@ -607,7 +642,7 @@ pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
usk: &UnifiedSpendingKey,
ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, N>,
) -> Result<NonEmpty<TxId>, ErrorT<DbT, InputsErrT, FeeRuleT>>
) -> Result<NonEmpty<TxId>, CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>>
where
DbT: WalletWrite + WalletCommitmentTrees,
ParamsT: consensus::Parameters + Clone,
@ -691,7 +726,7 @@ where
// `TransparentAddress` and `Outpoint`.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_prover: &impl SpendProver,
@ -707,7 +742,10 @@ fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
StepOutput,
(TransparentAddress, OutPoint),
>,
) -> Result<StepResult<<DbT as WalletRead>::AccountId>, ErrorT<DbT, InputsErrT, FeeRuleT>>
) -> Result<
StepResult<<DbT as WalletRead>::AccountId>,
CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
>
where
DbT: WalletWrite + WalletCommitmentTrees,
ParamsT: consensus::Parameters + Clone,
@ -749,53 +787,54 @@ where
return Err(Error::ProposalNotSupported);
}
let (sapling_anchor, sapling_inputs) =
if proposal_step.involves(PoolType::Shielded(ShieldedProtocol::Sapling)) {
proposal_step.shielded_inputs().map_or_else(
|| Ok((Some(sapling::Anchor::empty_tree()), vec![])),
|inputs| {
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
let anchor = sapling_tree
.root_at_checkpoint_id(&inputs.anchor_height())?
.ok_or(ProposalError::AnchorNotFound(inputs.anchor_height()))?
.into();
let (sapling_anchor, sapling_inputs) = if proposal_step
.involves(PoolType::Shielded(ShieldedProtocol::Sapling))
{
proposal_step.shielded_inputs().map_or_else(
|| Ok((Some(sapling::Anchor::empty_tree()), vec![])),
|inputs| {
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|sapling_tree| {
let anchor = sapling_tree
.root_at_checkpoint_id(&inputs.anchor_height())?
.ok_or(ProposalError::AnchorNotFound(inputs.anchor_height()))?
.into();
let sapling_inputs = inputs
.notes()
.iter()
.filter_map(|selected| match selected.note() {
Note::Sapling(note) => {
let key = match selected.spending_key_scope() {
Scope::External => usk.sapling().clone(),
Scope::Internal => usk.sapling().derive_internal(),
};
let sapling_inputs = inputs
.notes()
.iter()
.filter_map(|selected| match selected.note() {
Note::Sapling(note) => {
let key = match selected.spending_key_scope() {
Scope::External => usk.sapling().clone(),
Scope::Internal => usk.sapling().derive_internal(),
};
sapling_tree
.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&inputs.anchor_height(),
)
.and_then(|witness| {
witness.ok_or(ShardTreeError::Query(
QueryError::CheckpointPruned,
))
})
.map(|merkle_path| Some((key, note, merkle_path)))
.map_err(Error::from)
.transpose()
}
#[cfg(feature = "orchard")]
Note::Orchard(_) => None,
})
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
sapling_tree
.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&inputs.anchor_height(),
)
.and_then(|witness| {
witness.ok_or(ShardTreeError::Query(
QueryError::CheckpointPruned,
))
})
.map(|merkle_path| Some((key, note, merkle_path)))
.map_err(Error::from)
.transpose()
}
#[cfg(feature = "orchard")]
Note::Orchard(_) => None,
})
.collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()?;
Ok((Some(anchor), sapling_inputs))
})
},
)?
} else {
(None, vec![])
};
Ok((Some(anchor), sapling_inputs))
})
},
)?
} else {
(None, vec![])
};
#[cfg(feature = "orchard")]
let (orchard_anchor, orchard_inputs) = if proposal_step
@ -804,7 +843,7 @@ where
proposal_step.shielded_inputs().map_or_else(
|| Ok((Some(orchard::Anchor::empty_tree()), vec![])),
|inputs| {
wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _>>(|orchard_tree| {
wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|orchard_tree| {
let anchor = orchard_tree
.root_at_checkpoint_id(&inputs.anchor_height())?
.ok_or(ProposalError::AnchorNotFound(inputs.anchor_height()))?
@ -829,7 +868,7 @@ where
.transpose(),
Note::Sapling(_) => None,
})
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
.collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()?;
Ok((Some(anchor), orchard_inputs))
})
@ -872,7 +911,7 @@ where
#[cfg(feature = "transparent-inputs")]
let mut metadata_from_address = |addr: TransparentAddress| -> Result<
TransparentAddressMetadata,
ErrorT<DbT, InputsErrT, FeeRuleT>,
CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
> {
match cache.get(&addr) {
Some(result) => Ok(result.clone()),
@ -898,22 +937,23 @@ where
#[cfg(feature = "transparent-inputs")]
let utxos_spent = {
let mut utxos_spent: Vec<OutPoint> = vec![];
let add_transparent_input = |builder: &mut Builder<_, _>,
utxos_spent: &mut Vec<_>,
address_metadata: &TransparentAddressMetadata,
outpoint: OutPoint,
txout: TxOut|
-> Result<(), ErrorT<DbT, InputsErrT, FeeRuleT>> {
let secret_key = usk
.transparent()
.derive_secret_key(address_metadata.scope(), address_metadata.address_index())
.expect("spending key derivation should not fail");
let add_transparent_input =
|builder: &mut Builder<_, _>,
utxos_spent: &mut Vec<_>,
address_metadata: &TransparentAddressMetadata,
outpoint: OutPoint,
txout: TxOut|
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
let secret_key = usk
.transparent()
.derive_secret_key(address_metadata.scope(), address_metadata.address_index())
.expect("spending key derivation should not fail");
utxos_spent.push(outpoint.clone());
builder.add_transparent_input(secret_key, outpoint, txout)?;
utxos_spent.push(outpoint.clone());
builder.add_transparent_input(secret_key, outpoint, txout)?;
Ok(())
};
Ok(())
};
for utxo in proposal_step.transparent_inputs() {
add_transparent_input(
@ -1031,7 +1071,10 @@ where
let add_sapling_output = |builder: &mut Builder<_, _>,
sapling_output_meta: &mut Vec<_>,
to: sapling::PaymentAddress|
-> Result<(), ErrorT<DbT, InputsErrT, FeeRuleT>> {
-> Result<
(),
CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
> {
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
builder.add_sapling_output(sapling_external_ovk, to, payment.amount(), memo.clone())?;
sapling_output_meta.push((
@ -1043,50 +1086,52 @@ where
};
#[cfg(feature = "orchard")]
let add_orchard_output = |builder: &mut Builder<_, _>,
orchard_output_meta: &mut Vec<_>,
to: orchard::Address|
-> Result<(), ErrorT<DbT, InputsErrT, FeeRuleT>> {
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
builder.add_orchard_output(
orchard_external_ovk.clone(),
to,
payment.amount().into(),
memo.clone(),
)?;
orchard_output_meta.push((
Recipient::External(recipient_address.clone(), PoolType::ORCHARD),
payment.amount(),
Some(memo),
));
Ok(())
};
let add_orchard_output =
|builder: &mut Builder<_, _>,
orchard_output_meta: &mut Vec<_>,
to: orchard::Address|
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
builder.add_orchard_output(
orchard_external_ovk.clone(),
to,
payment.amount().into(),
memo.clone(),
)?;
orchard_output_meta.push((
Recipient::External(recipient_address.clone(), PoolType::ORCHARD),
payment.amount(),
Some(memo),
));
Ok(())
};
let add_transparent_output = |builder: &mut Builder<_, _>,
transparent_output_meta: &mut Vec<_>,
to: TransparentAddress|
-> Result<(), ErrorT<DbT, InputsErrT, FeeRuleT>> {
// Always reject sending to one of our known ephemeral addresses.
#[cfg(feature = "transparent-inputs")]
if wallet_db
.find_account_for_ephemeral_address(&to)
.map_err(Error::DataSource)?
.is_some()
{
return Err(Error::PaysEphemeralTransparentAddress(to.encode(params)));
}
if payment.memo().is_some() {
return Err(Error::MemoForbidden);
}
builder.add_transparent_output(&to, payment.amount())?;
transparent_output_meta.push((
Recipient::External(recipient_address.clone(), PoolType::TRANSPARENT),
to,
payment.amount(),
StepOutputIndex::Payment(payment_index),
));
Ok(())
};
let add_transparent_output =
|builder: &mut Builder<_, _>,
transparent_output_meta: &mut Vec<_>,
to: TransparentAddress|
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
// Always reject sending to one of our known ephemeral addresses.
#[cfg(feature = "transparent-inputs")]
if wallet_db
.find_account_for_ephemeral_address(&to)
.map_err(Error::DataSource)?
.is_some()
{
return Err(Error::PaysEphemeralTransparentAddress(to.encode(params)));
}
if payment.memo().is_some() {
return Err(Error::MemoForbidden);
}
builder.add_transparent_output(&to, payment.amount())?;
transparent_output_meta.push((
Recipient::External(recipient_address.clone(), PoolType::TRANSPARENT),
to,
payment.amount(),
StepOutputIndex::Payment(payment_index),
));
Ok(())
};
match recipient_address
.clone()
@ -1371,36 +1416,33 @@ where
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn shield_transparent_funds<DbT, ParamsT, InputsT>(
pub fn shield_transparent_funds<DbT, ParamsT, InputsT, ChangeT>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
input_selector: &InputsT,
change_strategy: &ChangeT,
shielding_threshold: NonNegativeAmount,
usk: &UnifiedSpendingKey,
from_addrs: &[TransparentAddress],
to_account: <DbT as InputSource>::AccountId,
min_confirmations: u32,
) -> Result<
NonEmpty<TxId>,
Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
InputsT::Error,
<InputsT::FeeRule as FeeRule>::Error,
>,
>
) -> Result<NonEmpty<TxId>, ShieldErrT<DbT, InputsT, ChangeT>>
where
ParamsT: consensus::Parameters,
DbT: WalletWrite + WalletCommitmentTrees + InputSource<Error = <DbT as WalletRead>::Error>,
InputsT: ShieldingSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let proposal = propose_shielding(
wallet_db,
params,
input_selector,
change_strategy,
shielding_threshold,
from_addrs,
to_account,
min_confirmations,
)?;

View File

@ -11,19 +11,16 @@ use nonempty::NonEmpty;
use zcash_address::ConversionError;
use zcash_primitives::{
consensus::{self, BlockHeight},
transaction::{
components::{
amount::{BalanceError, NonNegativeAmount},
TxOut,
},
fees::FeeRule,
transaction::components::{
amount::{BalanceError, NonNegativeAmount},
TxOut,
},
};
use crate::{
address::{Address, UnifiedAddress},
data_api::{InputSource, SimpleNoteRetention, SpendableNotes},
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy},
fees::{sapling, ChangeError, ChangeStrategy},
proposal::{Proposal, ProposalError, ShieldedInputs},
wallet::WalletTransparentOutput,
zip321::TransactionRequest,
@ -47,11 +44,13 @@ use crate::fees::orchard as orchard_fees;
/// The type of errors that may be produced in input selection.
#[derive(Debug)]
pub enum InputSelectorError<DbErrT, SelectorErrT> {
pub enum InputSelectorError<DbErrT, SelectorErrT, ChangeErrT, N> {
/// An error occurred accessing the underlying data store.
DataSource(DbErrT),
/// An error occurred specific to the provided input selector's selection rules.
Selection(SelectorErrT),
/// An error occurred in computing the change or fee for the proposed transfer.
Change(ChangeError<ChangeErrT, N>),
/// Input selection attempted to generate an invalid transaction proposal.
Proposal(ProposalError),
/// An error occurred parsing the address from a payment request.
@ -67,13 +66,9 @@ pub enum InputSelectorError<DbErrT, SelectorErrT> {
SyncRequired,
}
impl<E, S> From<ConversionError<&'static str>> for InputSelectorError<E, S> {
fn from(value: ConversionError<&'static str>) -> Self {
InputSelectorError::Address(value)
}
}
impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE, SE> {
impl<DE: fmt::Display, SE: fmt::Display, CE: fmt::Display, N: fmt::Display> fmt::Display
for InputSelectorError<DE, SE, CE, N>
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
InputSelectorError::DataSource(e) => {
@ -86,6 +81,11 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
InputSelectorError::Selection(e) => {
write!(f, "Note selection encountered the following error: {}", e)
}
InputSelectorError::Change(e) => write!(
f,
"Proposal generation failed due to an error in computing change or transaction fees: {}",
e
),
InputSelectorError::Proposal(e) => {
write!(
f,
@ -116,21 +116,36 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
}
}
impl<DE, SE> error::Error for InputSelectorError<DE, SE>
impl<DE, SE, CE, N> error::Error for InputSelectorError<DE, SE, CE, N>
where
DE: Debug + Display + error::Error + 'static,
SE: Debug + Display + error::Error + 'static,
CE: Debug + Display + error::Error + 'static,
N: Debug + Display + 'static,
{
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self {
Self::DataSource(e) => Some(e),
Self::Selection(e) => Some(e),
Self::Change(e) => Some(e),
Self::Proposal(e) => Some(e),
_ => None,
}
}
}
impl<E, S, F, N> From<ConversionError<&'static str>> for InputSelectorError<E, S, F, N> {
fn from(value: ConversionError<&'static str>) -> Self {
InputSelectorError::Address(value)
}
}
impl<E, S, C, N> From<ChangeError<C, N>> for InputSelectorError<E, S, C, N> {
fn from(err: ChangeError<C, N>) -> Self {
InputSelectorError::Change(err)
}
}
/// A strategy for selecting transaction inputs and proposing transaction outputs.
///
/// Proposals should include only economically useful inputs, as determined by `Self::FeeRule`;
@ -139,14 +154,13 @@ where
pub trait InputSelector {
/// The type of errors that may be generated in input selection
type Error;
/// The type of data source that the input selector expects to access to obtain input Sapling
/// notes. This associated type permits input selectors that may use specialized knowledge of
/// the internals of a particular backing data store, if the generic API of
/// `InputSource` does not provide sufficiently fine-grained operations for a particular
/// backing store to optimally perform input selection.
/// The type of data source that the input selector expects to access to obtain input notes.
/// This associated type permits input selectors that may use specialized knowledge of the
/// internals of a particular backing data store, if the generic API of `InputSource` does not
/// provide sufficiently fine-grained operations for a particular backing store to optimally
/// perform input selection.
type InputSource: InputSource;
/// The type of the fee rule that this input selector uses when computing fees.
type FeeRule: FeeRule;
/// Performs input selection and returns a proposal for transaction construction including
/// change and fee outputs.
@ -163,7 +177,8 @@ pub trait InputSelector {
/// If insufficient funds are available to satisfy the required outputs for the shielding
/// request, this operation must fail and return [`InputSelectorError::InsufficientFunds`].
#[allow(clippy::type_complexity)]
fn propose_transaction<ParamsT>(
#[allow(clippy::too_many_arguments)]
fn propose_transaction<ParamsT, ChangeT>(
&self,
params: &ParamsT,
wallet_db: &Self::InputSource,
@ -171,12 +186,19 @@ pub trait InputSelector {
anchor_height: BlockHeight,
account: <Self::InputSource as InputSource>::AccountId,
transaction_request: TransactionRequest,
change_strategy: &ChangeT,
) -> Result<
Proposal<Self::FeeRule, <Self::InputSource as InputSource>::NoteRef>,
InputSelectorError<<Self::InputSource as InputSource>::Error, Self::Error>,
Proposal<<ChangeT as ChangeStrategy>::FeeRule, <Self::InputSource as InputSource>::NoteRef>,
InputSelectorError<
<Self::InputSource as InputSource>::Error,
Self::Error,
ChangeT::Error,
<Self::InputSource as InputSource>::NoteRef,
>,
>
where
ParamsT: consensus::Parameters;
ParamsT: consensus::Parameters,
ChangeT: ChangeStrategy<MetaSource = Self::InputSource>;
}
/// A strategy for selecting transaction inputs and proposing transaction outputs
@ -192,8 +214,6 @@ pub trait ShieldingSelector {
/// [`InputSource`] does not provide sufficiently fine-grained operations for a
/// particular backing store to optimally perform input selection.
type InputSource: InputSource;
/// The type of the fee rule that this input selector uses when computing fees.
type FeeRule: FeeRule;
/// Performs input selection and returns a proposal for the construction of a shielding
/// transaction.
@ -204,36 +224,43 @@ pub trait ShieldingSelector {
/// outputs for the shielding request, this operation must fail and return
/// [`InputSelectorError::InsufficientFunds`].
#[allow(clippy::type_complexity)]
fn propose_shielding<ParamsT>(
#[allow(clippy::too_many_arguments)]
fn propose_shielding<ParamsT, ChangeT>(
&self,
params: &ParamsT,
wallet_db: &Self::InputSource,
change_strategy: &ChangeT,
shielding_threshold: NonNegativeAmount,
source_addrs: &[TransparentAddress],
to_account: <Self::InputSource as InputSource>::AccountId,
target_height: BlockHeight,
min_confirmations: u32,
) -> Result<
Proposal<Self::FeeRule, Infallible>,
InputSelectorError<<Self::InputSource as InputSource>::Error, Self::Error>,
Proposal<<ChangeT as ChangeStrategy>::FeeRule, Infallible>,
InputSelectorError<
<Self::InputSource as InputSource>::Error,
Self::Error,
ChangeT::Error,
Infallible,
>,
>
where
ParamsT: consensus::Parameters;
ParamsT: consensus::Parameters,
ChangeT: ChangeStrategy<MetaSource = Self::InputSource>;
}
/// Errors that can occur as a consequence of greedy input selection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT> {
pub enum GreedyInputSelectorError {
/// An intermediate value overflowed or underflowed the valid monetary range.
Balance(BalanceError),
/// A unified address did not contain a supported receiver.
UnsupportedAddress(Box<UnifiedAddress>),
/// Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature.
UnsupportedTexAddress,
/// An error was encountered in change selection.
Change(ChangeError<ChangeStrategyErrT, NoteRefT>),
}
impl<CE: fmt::Display, N: fmt::Display> fmt::Display for GreedyInputSelectorError<CE, N> {
impl fmt::Display for GreedyInputSelectorError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
GreedyInputSelectorError::Balance(e) => write!(
@ -249,32 +276,20 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for GreedyInputSelectorErro
GreedyInputSelectorError::UnsupportedTexAddress => {
write!(f, "Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature.")
}
GreedyInputSelectorError::Change(err) => {
write!(f, "An error occurred computing change and fees: {}", err)
}
}
}
}
impl<DbErrT, ChangeStrategyErrT, NoteRefT>
From<GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
for InputSelectorError<DbErrT, GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
impl<DbErrT, ChangeErrT, N> From<GreedyInputSelectorError>
for InputSelectorError<DbErrT, GreedyInputSelectorError, ChangeErrT, N>
{
fn from(err: GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>) -> Self {
fn from(err: GreedyInputSelectorError) -> Self {
InputSelectorError::Selection(err)
}
}
impl<DbErrT, ChangeStrategyErrT, NoteRefT> From<ChangeError<ChangeStrategyErrT, NoteRefT>>
for InputSelectorError<DbErrT, GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
{
fn from(err: ChangeError<ChangeStrategyErrT, NoteRefT>) -> Self {
InputSelectorError::Selection(GreedyInputSelectorError::Change(err))
}
}
impl<DbErrT, ChangeStrategyErrT, NoteRefT> From<BalanceError>
for InputSelectorError<DbErrT, GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
impl<DbErrT, ChangeErrT, N> From<BalanceError>
for InputSelectorError<DbErrT, GreedyInputSelectorError, ChangeErrT, N>
{
fn from(err: BalanceError) -> Self {
InputSelectorError::Selection(GreedyInputSelectorError::Balance(err))
@ -319,13 +334,11 @@ impl orchard_fees::OutputView for OrchardPayment {
///
/// This implementation performs input selection using methods available via the
/// [`InputSource`] interface.
pub struct GreedyInputSelector<DbT, ChangeT> {
change_strategy: ChangeT,
dust_output_policy: DustOutputPolicy,
pub struct GreedyInputSelector<DbT> {
_ds_type: PhantomData<DbT>,
}
impl<DbT, ChangeT: ChangeStrategy> GreedyInputSelector<DbT, ChangeT> {
impl<DbT> GreedyInputSelector<DbT> {
/// Constructs a new greedy input selector that uses the provided change strategy to determine
/// change values and fee amounts.
///
@ -335,27 +348,25 @@ impl<DbT, ChangeT: ChangeStrategy> GreedyInputSelector<DbT, ChangeT> {
/// attempting to construct a transaction proposal that requires such an output.
///
/// [`EphemeralBalance::Output`]: crate::fees::EphemeralBalance::Output
pub fn new(change_strategy: ChangeT, dust_output_policy: DustOutputPolicy) -> Self {
pub fn new() -> Self {
GreedyInputSelector {
change_strategy,
dust_output_policy,
_ds_type: PhantomData,
}
}
}
impl<DbT, ChangeT> InputSelector for GreedyInputSelector<DbT, ChangeT>
where
DbT: InputSource,
ChangeT: ChangeStrategy,
ChangeT::FeeRule: Clone,
{
type Error = GreedyInputSelectorError<ChangeT::Error, DbT::NoteRef>;
impl<DbT> Default for GreedyInputSelector<DbT> {
fn default() -> Self {
Self::new()
}
}
impl<DbT: InputSource> InputSelector for GreedyInputSelector<DbT> {
type Error = GreedyInputSelectorError;
type InputSource = DbT;
type FeeRule = ChangeT::FeeRule;
#[allow(clippy::type_complexity)]
fn propose_transaction<ParamsT>(
fn propose_transaction<ParamsT, ChangeT>(
&self,
params: &ParamsT,
wallet_db: &Self::InputSource,
@ -363,13 +374,15 @@ where
anchor_height: BlockHeight,
account: <DbT as InputSource>::AccountId,
transaction_request: TransactionRequest,
change_strategy: &ChangeT,
) -> Result<
Proposal<Self::FeeRule, DbT::NoteRef>,
InputSelectorError<<DbT as InputSource>::Error, Self::Error>,
Proposal<<ChangeT as ChangeStrategy>::FeeRule, DbT::NoteRef>,
InputSelectorError<<DbT as InputSource>::Error, Self::Error, ChangeT::Error, DbT::NoteRef>,
>
where
ParamsT: consensus::Parameters,
Self::InputSource: InputSource,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let mut transparent_outputs = vec![];
let mut sapling_outputs = vec![];
@ -484,8 +497,8 @@ where
// catching the `InsufficientFunds` error to obtain the required amount
// given the provided change strategy. Ignore the change memo in order
// to avoid adding a change output.
let tr1_required_input_value =
match self.change_strategy.compute_balance::<_, DbT::NoteRef>(
let tr1_required_input_value = match change_strategy
.compute_balance::<_, DbT::NoteRef>(
params,
target_height,
&[] as &[WalletTransparentOutput],
@ -493,17 +506,18 @@ where
&sapling::EmptyBundleView,
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&self.dust_output_policy,
Some(&EphemeralBalance::Input(NonNegativeAmount::ZERO)),
None,
) {
Err(ChangeError::InsufficientFunds { required, .. }) => required,
Ok(_) => NonNegativeAmount::ZERO, // shouldn't happen
Err(other) => return Err(other.into()),
};
Err(ChangeError::InsufficientFunds { required, .. }) => required,
Err(ChangeError::DustInputs { .. }) => unreachable!("no inputs were supplied"),
Err(other) => return Err(InputSelectorError::Change(other)),
Ok(_) => NonNegativeAmount::ZERO, // shouldn't happen
};
// Now recompute to obtain the `TransactionBalance` and verify that it
// fully accounts for the required fees.
let tr1_balance = self.change_strategy.compute_balance::<_, DbT::NoteRef>(
let tr1_balance = change_strategy.compute_balance::<_, DbT::NoteRef>(
params,
target_height,
&[] as &[WalletTransparentOutput],
@ -511,8 +525,8 @@ where
&sapling::EmptyBundleView,
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&self.dust_output_policy,
Some(&EphemeralBalance::Input(tr1_required_input_value)),
None,
)?;
assert_eq!(tr1_balance.total(), tr1_balance.fee_required());
@ -573,9 +587,20 @@ where
vec![]
};
let selected_input_ids = sapling_inputs.iter().map(|(id, _)| id);
#[cfg(feature = "orchard")]
let selected_input_ids =
selected_input_ids.chain(orchard_inputs.iter().map(|(id, _)| id));
let selected_input_ids = selected_input_ids.cloned().collect::<Vec<_>>();
let wallet_meta = change_strategy
.fetch_wallet_meta(wallet_db, account, &selected_input_ids)
.map_err(InputSelectorError::DataSource)?;
// In the ZIP 320 case, this is the balance for transaction 0, taking into account
// the ephemeral output.
let balance = self.change_strategy.compute_balance(
let balance = change_strategy.compute_balance(
params,
target_height,
&[] as &[WalletTransparentOutput],
@ -591,8 +616,8 @@ where
&orchard_inputs[..],
&orchard_outputs[..],
),
&self.dust_output_policy,
ephemeral_balance.as_ref(),
Some(&wallet_meta),
);
match balance {
@ -681,7 +706,7 @@ where
);
return Proposal::multi_step(
self.change_strategy.fee_rule().clone(),
change_strategy.fee_rule().clone(),
target_height,
NonEmpty::from_vec(steps).expect("steps is known to be nonempty"),
)
@ -694,7 +719,7 @@ where
vec![],
shielded_inputs,
balance,
self.change_strategy.fee_rule().clone(),
(*change_strategy.fee_rule()).clone(),
target_height,
false,
)
@ -713,7 +738,7 @@ where
Err(ChangeError::InsufficientFunds { required, .. }) => {
amount_required = required;
}
Err(other) => return Err(other.into()),
Err(other) => return Err(InputSelectorError::Change(other)),
}
#[cfg(not(feature = "orchard"))]
@ -747,31 +772,28 @@ where
}
#[cfg(feature = "transparent-inputs")]
impl<DbT, ChangeT> ShieldingSelector for GreedyInputSelector<DbT, ChangeT>
where
DbT: InputSource,
ChangeT: ChangeStrategy,
ChangeT::FeeRule: Clone,
{
type Error = GreedyInputSelectorError<ChangeT::Error, Infallible>;
impl<DbT: InputSource> ShieldingSelector for GreedyInputSelector<DbT> {
type Error = GreedyInputSelectorError;
type InputSource = DbT;
type FeeRule = ChangeT::FeeRule;
#[allow(clippy::type_complexity)]
fn propose_shielding<ParamsT>(
fn propose_shielding<ParamsT, ChangeT>(
&self,
params: &ParamsT,
wallet_db: &Self::InputSource,
change_strategy: &ChangeT,
shielding_threshold: NonNegativeAmount,
source_addrs: &[TransparentAddress],
to_account: <Self::InputSource as InputSource>::AccountId,
target_height: BlockHeight,
min_confirmations: u32,
) -> Result<
Proposal<Self::FeeRule, Infallible>,
InputSelectorError<<DbT as InputSource>::Error, Self::Error>,
Proposal<<ChangeT as ChangeStrategy>::FeeRule, Infallible>,
InputSelectorError<<DbT as InputSource>::Error, Self::Error, ChangeT::Error, Infallible>,
>
where
ParamsT: consensus::Parameters,
ChangeT: ChangeStrategy<MetaSource = Self::InputSource>,
{
let mut transparent_inputs: Vec<WalletTransparentOutput> = source_addrs
.iter()
@ -784,7 +806,11 @@ where
.flat_map(|v| v.into_iter())
.collect();
let trial_balance = self.change_strategy.compute_balance(
let wallet_meta = change_strategy
.fetch_wallet_meta(wallet_db, to_account, &[])
.map_err(InputSelectorError::DataSource)?;
let trial_balance = change_strategy.compute_balance(
params,
target_height,
&transparent_inputs,
@ -792,8 +818,8 @@ where
&sapling::EmptyBundleView,
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&self.dust_output_policy,
None,
Some(&wallet_meta),
);
let balance = match trial_balance {
@ -802,7 +828,7 @@ where
let exclusions: BTreeSet<OutPoint> = transparent.into_iter().collect();
transparent_inputs.retain(|i| !exclusions.contains(i.outpoint()));
self.change_strategy.compute_balance(
change_strategy.compute_balance(
params,
target_height,
&transparent_inputs,
@ -810,13 +836,11 @@ where
&sapling::EmptyBundleView,
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&self.dust_output_policy,
None,
Some(&wallet_meta),
)?
}
Err(other) => {
return Err(other.into());
}
Err(other) => return Err(InputSelectorError::Change(other)),
};
if balance.total() >= shielding_threshold {
@ -826,7 +850,7 @@ where
transparent_inputs,
None,
balance,
(*self.change_strategy.fee_rule()).clone(),
(*change_strategy.fee_rule()).clone(),
target_height,
true,
)

View File

@ -1,18 +1,17 @@
use std::fmt;
use std::fmt::{self, Debug, Display};
use zcash_primitives::{
consensus::{self, BlockHeight},
memo::MemoBytes,
transaction::{
components::{
amount::{BalanceError, NonNegativeAmount},
OutPoint,
},
components::{amount::NonNegativeAmount, OutPoint},
fees::{transparent, FeeRule},
},
};
use zcash_protocol::{PoolType, ShieldedProtocol};
use crate::data_api::InputSource;
pub(crate) mod common;
pub mod fixed;
#[cfg(feature = "orchard")]
@ -273,9 +272,16 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for ChangeError<CE, N> {
}
}
impl<NoteRefT> From<BalanceError> for ChangeError<BalanceError, NoteRefT> {
fn from(err: BalanceError) -> ChangeError<BalanceError, NoteRefT> {
ChangeError::StrategyError(err)
impl<E, N> std::error::Error for ChangeError<E, N>
where
E: Debug + Display + std::error::Error + 'static,
N: Debug + Display + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self {
ChangeError::StrategyError(e) => Some(e),
_ => None,
}
}
}
@ -368,13 +374,30 @@ impl EphemeralBalance {
/// A trait that represents the ability to compute the suggested change and fees that must be paid
/// by a transaction having a specified set of inputs and outputs.
pub trait ChangeStrategy {
type FeeRule: FeeRule;
type FeeRule: FeeRule + Clone;
type Error;
/// The type of metadata source that this change strategy requires in order to be able to
/// retrieve required wallet metadata. If more capabilities are required of the backend than
/// are exposed in the [`InputSource`] trait, the implementer of this trait should define their
/// own trait that descends from [`InputSource`] and adds the required capabilities there, and
/// then implement that trait for their desired database backend.
type MetaSource: InputSource;
type WalletMeta;
/// Returns the fee rule that this change strategy will respect when performing
/// balance computations.
fn fee_rule(&self) -> &Self::FeeRule;
/// Uses the provided metadata source to obtain the wallet metadata required for change
/// creation determinations.
fn fetch_wallet_meta(
&self,
meta_source: &Self::MetaSource,
account: <Self::MetaSource as InputSource>::AccountId,
exclude: &[<Self::MetaSource as InputSource>::NoteRef],
) -> Result<Self::WalletMeta, <Self::MetaSource as InputSource>::Error>;
/// Computes the totals of inputs, suggested change amounts, and fees given the
/// provided inputs and outputs being used to construct a transaction.
///
@ -393,7 +416,11 @@ pub trait ChangeStrategy {
/// - `ephemeral_balance`: if the transaction is to be constructed with either an
/// ephemeral transparent input or an ephemeral transparent output this argument
/// may be used to provide the value of that input or output. The value of this
/// output should be `None` in the case that there are no such items.
/// argument should be `None` in the case that there are no such items.
/// - `wallet_meta`: Additional wallet metadata that the change strategy may use
/// in determining how to construct change outputs. This wallet metadata value
/// should be computed excluding the inputs provided in the `transparent_inputs`,
/// `sapling`, and `orchard` arguments.
///
/// [ZIP 320]: https://zips.z.cash/zip-0320
#[allow(clippy::too_many_arguments)]
@ -405,8 +432,8 @@ pub trait ChangeStrategy {
transparent_outputs: &[impl transparent::OutputView],
sapling: &impl sapling::BundleView<NoteRefT>,
#[cfg(feature = "orchard")] orchard: &impl orchard::BundleView<NoteRefT>,
dust_output_policy: &DustOutputPolicy,
ephemeral_balance: Option<&EphemeralBalance>,
wallet_meta: Option<&Self::WalletMeta>,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>;
}

View File

@ -1,5 +1,7 @@
//! Change strategies designed for use with a fixed fee.
use std::marker::PhantomData;
use zcash_primitives::{
consensus::{self, BlockHeight},
memo::MemoBytes,
@ -9,7 +11,7 @@ use zcash_primitives::{
},
};
use crate::ShieldedProtocol;
use crate::{data_api::InputSource, ShieldedProtocol};
use super::{
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
@ -23,13 +25,15 @@ use super::orchard as orchard_fees;
/// as the most current pool that avoids unnecessary pool-crossing (with a specified
/// fallback when the transaction has no shielded inputs). Fee calculation is delegated
/// to the provided fee rule.
pub struct SingleOutputChangeStrategy {
pub struct SingleOutputChangeStrategy<I> {
fee_rule: FixedFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
dust_output_policy: DustOutputPolicy,
meta_source: PhantomData<I>,
}
impl SingleOutputChangeStrategy {
impl<I> SingleOutputChangeStrategy<I> {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule
/// and change memo.
///
@ -39,23 +43,37 @@ impl SingleOutputChangeStrategy {
fee_rule: FixedFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
dust_output_policy: DustOutputPolicy,
) -> Self {
Self {
fee_rule,
change_memo,
fallback_change_pool,
dust_output_policy,
meta_source: PhantomData,
}
}
}
impl ChangeStrategy for SingleOutputChangeStrategy {
impl<I: InputSource> ChangeStrategy for SingleOutputChangeStrategy<I> {
type FeeRule = FixedFeeRule;
type Error = BalanceError;
type MetaSource = I;
type WalletMeta = ();
fn fee_rule(&self) -> &Self::FeeRule {
&self.fee_rule
}
fn fetch_wallet_meta(
&self,
_meta_source: &Self::MetaSource,
_account: <Self::MetaSource as InputSource>::AccountId,
_exclude: &[<Self::MetaSource as crate::data_api::InputSource>::NoteRef],
) -> Result<Self::WalletMeta, <Self::MetaSource as crate::data_api::InputSource>::Error> {
Ok(())
}
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
&self,
params: &P,
@ -64,8 +82,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
transparent_outputs: &[impl transparent::OutputView],
sapling: &impl sapling_fees::BundleView<NoteRefT>,
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
dust_output_policy: &DustOutputPolicy,
ephemeral_balance: Option<&EphemeralBalance>,
_wallet_meta: Option<&Self::WalletMeta>,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
single_change_output_balance(
params,
@ -76,7 +94,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
sapling,
#[cfg(feature = "orchard")]
orchard,
dust_output_policy,
&self.dust_output_policy,
self.fee_rule.fixed_fee(),
self.change_memo.as_ref(),
self.fallback_change_pool,
@ -99,7 +117,7 @@ mod tests {
use super::SingleOutputChangeStrategy;
use crate::{
data_api::wallet::input_selection::SaplingPayment,
data_api::{testing::MockWalletDb, wallet::input_selection::SaplingPayment},
fees::{
tests::{TestSaplingInput, TestTransparentInput},
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
@ -114,8 +132,12 @@ mod tests {
fn change_without_dust() {
#[allow(deprecated)]
let fee_rule = FixedFeeRule::standard();
let change_strategy =
SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling);
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
fee_rule,
None,
ShieldedProtocol::Sapling,
DustOutputPolicy::default(),
);
// spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance(
@ -137,7 +159,7 @@ mod tests {
),
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&DustOutputPolicy::default(),
None,
None,
);
@ -153,8 +175,12 @@ mod tests {
fn dust_change() {
#[allow(deprecated)]
let fee_rule = FixedFeeRule::standard();
let change_strategy =
SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling);
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
fee_rule,
None,
ShieldedProtocol::Sapling,
DustOutputPolicy::default(),
);
// spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance(
@ -183,7 +209,7 @@ mod tests {
),
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&DustOutputPolicy::default(),
None,
None,
);

View File

@ -1,5 +1,7 @@
//! Change strategies designed for use with a standard fee.
use std::marker::PhantomData;
use zcash_primitives::{
consensus::{self, BlockHeight},
memo::MemoBytes,
@ -14,7 +16,7 @@ use zcash_primitives::{
},
};
use crate::ShieldedProtocol;
use crate::{data_api::InputSource, ShieldedProtocol};
use super::{
fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy,
@ -28,13 +30,15 @@ use super::orchard as orchard_fees;
/// as the most current pool that avoids unnecessary pool-crossing (with a specified
/// fallback when the transaction has no shielded inputs). Fee calculation is delegated
/// to the provided fee rule.
pub struct SingleOutputChangeStrategy {
pub struct SingleOutputChangeStrategy<I> {
fee_rule: StandardFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
dust_output_policy: DustOutputPolicy,
meta_source: PhantomData<I>,
}
impl SingleOutputChangeStrategy {
impl<I> SingleOutputChangeStrategy<I> {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
/// fee parameters.
///
@ -44,23 +48,37 @@ impl SingleOutputChangeStrategy {
fee_rule: StandardFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
dust_output_policy: DustOutputPolicy,
) -> Self {
Self {
fee_rule,
change_memo,
fallback_change_pool,
dust_output_policy,
meta_source: PhantomData,
}
}
}
impl ChangeStrategy for SingleOutputChangeStrategy {
impl<I: InputSource> ChangeStrategy for SingleOutputChangeStrategy<I> {
type FeeRule = StandardFeeRule;
type Error = Zip317FeeError;
type MetaSource = I;
type WalletMeta = ();
fn fee_rule(&self) -> &Self::FeeRule {
&self.fee_rule
}
fn fetch_wallet_meta(
&self,
_meta_source: &Self::MetaSource,
_account: <Self::MetaSource as InputSource>::AccountId,
_exclude: &[<Self::MetaSource as crate::data_api::InputSource>::NoteRef],
) -> Result<Self::WalletMeta, <Self::MetaSource as crate::data_api::InputSource>::Error> {
Ok(())
}
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
&self,
params: &P,
@ -69,15 +87,16 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
transparent_outputs: &[impl transparent::OutputView],
sapling: &impl sapling_fees::BundleView<NoteRefT>,
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
dust_output_policy: &DustOutputPolicy,
ephemeral_balance: Option<&EphemeralBalance>,
wallet_meta: Option<&Self::WalletMeta>,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
#[allow(deprecated)]
match self.fee_rule() {
StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::new(
StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::<I>::new(
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(10000)),
self.change_memo.clone(),
self.fallback_change_pool,
self.dust_output_policy,
)
.compute_balance(
params,
@ -87,14 +106,15 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
sapling,
#[cfg(feature = "orchard")]
orchard,
dust_output_policy,
ephemeral_balance,
wallet_meta,
)
.map_err(|e| e.map(Zip317FeeError::Balance)),
StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::new(
StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::<I>::new(
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(1000)),
self.change_memo.clone(),
self.fallback_change_pool,
self.dust_output_policy,
)
.compute_balance(
params,
@ -104,14 +124,15 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
sapling,
#[cfg(feature = "orchard")]
orchard,
dust_output_policy,
ephemeral_balance,
wallet_meta,
)
.map_err(|e| e.map(Zip317FeeError::Balance)),
StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::new(
StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::<I>::new(
Zip317FeeRule::standard(),
self.change_memo.clone(),
self.fallback_change_pool,
self.dust_output_policy,
)
.compute_balance(
params,
@ -121,8 +142,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
sapling,
#[cfg(feature = "orchard")]
orchard,
dust_output_policy,
ephemeral_balance,
wallet_meta,
),
}
}

View File

@ -4,6 +4,8 @@
//! to ensure that inputs added to a transaction do not cause fees to rise by
//! an amount greater than their value.
use std::marker::PhantomData;
use zcash_primitives::{
consensus::{self, BlockHeight},
memo::MemoBytes,
@ -13,7 +15,7 @@ use zcash_primitives::{
},
};
use crate::ShieldedProtocol;
use crate::{data_api::InputSource, ShieldedProtocol};
use super::{
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
@ -27,13 +29,15 @@ use super::orchard as orchard_fees;
/// as the most current pool that avoids unnecessary pool-crossing (with a specified
/// fallback when the transaction has no shielded inputs). Fee calculation is delegated
/// to the provided fee rule.
pub struct SingleOutputChangeStrategy {
pub struct SingleOutputChangeStrategy<I> {
fee_rule: Zip317FeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
dust_output_policy: DustOutputPolicy,
meta_source: PhantomData<I>,
}
impl SingleOutputChangeStrategy {
impl<I> SingleOutputChangeStrategy<I> {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
/// fee parameters and change memo.
///
@ -43,23 +47,37 @@ impl SingleOutputChangeStrategy {
fee_rule: Zip317FeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
dust_output_policy: DustOutputPolicy,
) -> Self {
Self {
fee_rule,
change_memo,
fallback_change_pool,
dust_output_policy,
meta_source: PhantomData,
}
}
}
impl ChangeStrategy for SingleOutputChangeStrategy {
impl<I: InputSource> ChangeStrategy for SingleOutputChangeStrategy<I> {
type FeeRule = Zip317FeeRule;
type Error = Zip317FeeError;
type MetaSource = I;
type WalletMeta = ();
fn fee_rule(&self) -> &Self::FeeRule {
&self.fee_rule
}
fn fetch_wallet_meta(
&self,
_meta_source: &Self::MetaSource,
_account: <Self::MetaSource as InputSource>::AccountId,
_exclude: &[<Self::MetaSource as InputSource>::NoteRef],
) -> Result<Self::WalletMeta, <Self::MetaSource as InputSource>::Error> {
Ok(())
}
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
&self,
params: &P,
@ -68,8 +86,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
transparent_outputs: &[impl transparent::OutputView],
sapling: &impl sapling_fees::BundleView<NoteRefT>,
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
dust_output_policy: &DustOutputPolicy,
ephemeral_balance: Option<&EphemeralBalance>,
_wallet_meta: Option<&Self::WalletMeta>,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
single_change_output_balance(
params,
@ -80,7 +98,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
sapling,
#[cfg(feature = "orchard")]
orchard,
dust_output_policy,
&self.dust_output_policy,
self.fee_rule.marginal_fee(),
self.change_memo.as_ref(),
self.fallback_change_pool,
@ -106,7 +124,7 @@ mod tests {
use super::SingleOutputChangeStrategy;
use crate::{
data_api::wallet::input_selection::SaplingPayment,
data_api::{testing::MockWalletDb, wallet::input_selection::SaplingPayment},
fees::{
tests::{TestSaplingInput, TestTransparentInput},
ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy,
@ -122,10 +140,11 @@ mod tests {
#[test]
fn change_without_dust() {
let change_strategy = SingleOutputChangeStrategy::new(
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
DustOutputPolicy::default(),
);
// spend a single Sapling note that is sufficient to pay the fee
@ -148,7 +167,7 @@ mod tests {
),
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&DustOutputPolicy::default(),
None,
None,
);
@ -163,10 +182,11 @@ mod tests {
#[test]
#[cfg(feature = "orchard")]
fn cross_pool_change_without_dust() {
let change_strategy = SingleOutputChangeStrategy::new(
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Orchard,
DustOutputPolicy::default(),
);
// spend a single Sapling note that is sufficient to pay the fee
@ -192,7 +212,7 @@ mod tests {
30000,
))][..],
),
&DustOutputPolicy::default(),
None,
None,
);
@ -206,22 +226,23 @@ mod tests {
#[test]
fn change_with_transparent_payments_implicitly_allowing_zero_change() {
change_with_transparent_payments(&DustOutputPolicy::default())
change_with_transparent_payments(DustOutputPolicy::default())
}
#[test]
fn change_with_transparent_payments_explicitly_allowing_zero_change() {
change_with_transparent_payments(&DustOutputPolicy::new(
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(
fn change_with_transparent_payments(dust_output_policy: DustOutputPolicy) {
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
dust_output_policy,
);
// spend a single Sapling note that is sufficient to pay the fee
@ -245,7 +266,7 @@ mod tests {
),
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
dust_output_policy,
None,
None,
);
@ -263,10 +284,11 @@ mod tests {
use crate::fees::sapling as sapling_fees;
use zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint};
let change_strategy = SingleOutputChangeStrategy::new(
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
DustOutputPolicy::default(),
);
// Spend a single transparent UTXO that is exactly sufficient to pay the fee.
@ -289,7 +311,7 @@ mod tests {
&sapling_fees::EmptyBundleView,
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&DustOutputPolicy::default(),
None,
None,
);
@ -307,10 +329,11 @@ mod tests {
use crate::fees::sapling as sapling_fees;
use zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint};
let change_strategy = SingleOutputChangeStrategy::new(
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
DustOutputPolicy::default(),
);
// Spend a single transparent UTXO that is sufficient to pay the fee.
@ -333,7 +356,7 @@ mod tests {
&sapling_fees::EmptyBundleView,
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&DustOutputPolicy::default(),
None,
None,
);
@ -351,10 +374,14 @@ mod tests {
use crate::fees::sapling as sapling_fees;
use zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint};
let change_strategy = SingleOutputChangeStrategy::new(
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
DustOutputPolicy::new(
DustAction::AllowDustChange,
Some(NonNegativeAmount::const_from_u64(1000)),
),
);
// Spend a single transparent UTXO that is sufficient to pay the fee.
@ -380,10 +407,7 @@ mod tests {
&sapling_fees::EmptyBundleView,
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&DustOutputPolicy::new(
DustAction::AllowDustChange,
Some(NonNegativeAmount::const_from_u64(1000)),
),
None,
None,
);
@ -397,22 +421,23 @@ mod tests {
#[test]
fn change_with_allowable_dust_implicitly_allowing_zero_change() {
change_with_allowable_dust(&DustOutputPolicy::default())
change_with_allowable_dust(DustOutputPolicy::default())
}
#[test]
fn change_with_allowable_dust_explicitly_allowing_zero_change() {
change_with_allowable_dust(&DustOutputPolicy::new(
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(
fn change_with_allowable_dust(dust_output_policy: DustOutputPolicy) {
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
dust_output_policy,
);
// Spend two Sapling notes, one of them dust. There is sufficient to
@ -444,7 +469,7 @@ mod tests {
),
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
dust_output_policy,
None,
None,
);
@ -458,10 +483,11 @@ mod tests {
#[test]
fn change_with_disallowed_dust() {
let change_strategy = SingleOutputChangeStrategy::new(
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
DustOutputPolicy::default(),
);
// Attempt to spend three Sapling notes, one of them dust. Adding the third
@ -495,7 +521,7 @@ mod tests {
),
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
&DustOutputPolicy::default(),
None,
None,
);

View File

@ -126,7 +126,7 @@ impl Display for ProposalError {
#[cfg(feature = "transparent-inputs")]
ProposalError::EphemeralOutputsInvalid => write!(
f,
"The change strategy provided to input selection failed to correctly generate an ephemeral change output when needed for sending to a TEX address."
"The proposal generator failed to correctly generate an ephemeral change output when needed for sending to a TEX address."
),
}
}

View File

@ -118,7 +118,7 @@ pub(crate) fn external_address_change_spends_detected_in_restore_from_seed<
#[allow(dead_code)]
pub(crate) fn zip317_spend<T: ShieldedPoolTester>() {
zcash_client_backend::data_api::testing::pool::zip317_spend::<T>(
zcash_client_backend::data_api::testing::pool::zip317_spend::<T, TestDbFactory>(
TestDbFactory,
BlockCache::new(),
)

View File

@ -65,6 +65,7 @@
//! - `memo` the shielded memo associated with the output, if any.
use incrementalmerkletree::{Marking, Retention};
use rusqlite::{self, named_params, params, OptionalExtension};
use secrecy::{ExposeSecret, SecretVec};
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
@ -78,6 +79,7 @@ use std::convert::TryFrom;
use std::io::{self, Cursor};
use std::num::NonZeroU32;
use std::ops::RangeInclusive;
use tracing::{debug, warn};
use zcash_address::ZcashAddress;

View File

@ -1779,20 +1779,21 @@ pub(crate) mod tests {
fee_rule,
Some(change_memo.into()),
OrchardPoolTester::SHIELDED_PROTOCOL,
DustOutputPolicy::default(),
);
let input_selector =
&GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
let input_selector = GreedyInputSelector::new();
let proposal = st
.propose_transfer(
account.id(),
input_selector,
&input_selector,
&change_strategy,
request,
NonZeroU32::new(10).unwrap(),
)
.unwrap();
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal,
@ -1867,13 +1868,14 @@ pub(crate) mod tests {
fee_rule,
Some(change_memo.into()),
OrchardPoolTester::SHIELDED_PROTOCOL,
DustOutputPolicy::default(),
);
let input_selector =
&GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
let input_selector = GreedyInputSelector::new();
let proposal = st.propose_transfer(
account.id(),
input_selector,
&input_selector,
&change_strategy,
request.clone(),
NonZeroU32::new(10).unwrap(),
);
@ -1886,7 +1888,8 @@ pub(crate) mod tests {
// Verify that it's now possible to create the proposal
let proposal = st.propose_transfer(
account.id(),
input_selector,
&input_selector,
&change_strategy,
request,
NonZeroU32::new(10).unwrap(),
);