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` - `Progress`
- `WalletSummary::progress` - `WalletSummary::progress`
- `WalletMeta` - `WalletMeta`
- `impl Default for wallet::input_selection::GreedyInputSelector`
### Changed ### Changed
- `zcash_client_backend::data_api`: - `zcash_client_backend::data_api`:
- `InputSource` has an added method `get_wallet_metadata` - `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 ### Changed
- MSRV is now 1.77.0. - MSRV is now 1.77.0.
@ -25,6 +70,8 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::data_api`: - `zcash_client_backend::data_api`:
- `WalletSummary::scan_progress` and `WalletSummary::recovery_progress` have - `WalletSummary::scan_progress` and `WalletSummary::recovery_progress` have
been removed. Use `WalletSummary::progress` instead. been removed. Use `WalletSummary::progress` instead.
- `zcash_client_backend::fees`:
- `impl From<BalanceError> for ChangeError<...>`
## [0.14.0] - 2024-10-04 ## [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 /// 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. /// 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::address::UnifiedAddress;
use crate::data_api::wallet::input_selection::InputSelectorError; use crate::data_api::wallet::input_selection::InputSelectorError;
use crate::fees::ChangeError;
use crate::proposal::ProposalError; use crate::proposal::ProposalError;
use crate::PoolType; use crate::PoolType;
@ -23,7 +24,8 @@ use crate::wallet::NoteId;
/// Errors that can occur as a consequence of wallet operations. /// Errors that can occur as a consequence of wallet operations.
#[derive(Debug)] #[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 /// An error occurred retrieving data from the underlying data source
DataSource(DataSourceError), DataSource(DataSourceError),
@ -33,6 +35,9 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
/// An error in note selection /// An error in note selection
NoteSelection(SelectionError), NoteSelection(SelectionError),
/// An error in change selection during transaction proposal construction
Change(ChangeError<ChangeErrT, NoteRefT>),
/// An error in transaction proposal construction /// An error in transaction proposal construction
Proposal(ProposalError), Proposal(ProposalError),
@ -98,12 +103,14 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
PaysEphemeralTransparentAddress(String), 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 where
DE: fmt::Display, DE: fmt::Display,
CE: fmt::Display, TE: fmt::Display,
SE: fmt::Display, SE: fmt::Display,
FE: fmt::Display, FE: fmt::Display,
CE: fmt::Display,
N: fmt::Display,
{ {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use fmt::Write; use fmt::Write;
@ -122,6 +129,9 @@ where
Error::NoteSelection(e) => { Error::NoteSelection(e) => {
write!(f, "Note selection encountered the following error: {}", e) write!(f, "Note selection encountered the following error: {}", e)
} }
Error::Change(e) => {
write!(f, "Change output generation failed: {}", e)
}
Error::Proposal(e) => { Error::Proposal(e) => {
write!(f, "Input selection attempted to construct an invalid 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 where
DE: Debug + Display + error::Error + 'static, DE: Debug + Display + error::Error + 'static,
CE: Debug + Display + error::Error + 'static, TE: Debug + Display + error::Error + 'static,
SE: Debug + Display + error::Error + 'static, SE: Debug + Display + error::Error + 'static,
FE: Debug + Display + 'static, FE: Debug + Display + 'static,
CE: Debug + Display + error::Error + 'static,
N: Debug + Display + 'static,
{ {
fn source(&self) -> Option<&(dyn error::Error + 'static)> { fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self { 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 { fn from(e: builder::Error<FE>) -> Self {
Error::Builder(e) 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 { fn from(e: ProposalError) -> Self {
Error::Proposal(e) 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 { fn from(e: BalanceError) -> Self {
Error::BalanceError(e) 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 { fn from(value: ConversionError<&'static str>) -> Self {
Error::Address(value) Error::Address(value)
} }
} }
impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE> { impl<DE, TE, SE, FE, CE, N> From<InputSelectorError<DE, SE, CE, N>>
fn from(e: InputSelectorError<DE, SE>) -> Self { for Error<DE, TE, SE, FE, CE, N>
{
fn from(e: InputSelectorError<DE, SE, CE, N>) -> Self {
match e { match e {
InputSelectorError::DataSource(e) => Error::DataSource(e), InputSelectorError::DataSource(e) => Error::DataSource(e),
InputSelectorError::Selection(e) => Error::NoteSelection(e), InputSelectorError::Selection(e) => Error::NoteSelection(e),
InputSelectorError::Change(e) => Error::Change(e),
InputSelectorError::Proposal(e) => Error::Proposal(e), InputSelectorError::Proposal(e) => Error::Proposal(e),
InputSelectorError::InsufficientFunds { InputSelectorError::InsufficientFunds {
available, 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 { fn from(e: sapling::builder::Error) -> Self {
Error::Builder(builder::Error::SaplingBuild(e)) 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 { fn from(e: transparent::builder::Error) -> Self {
Error::Builder(builder::Error::TransparentBuild(e)) Error::Builder(builder::Error::TransparentBuild(e))
} }
} }
impl<DE, CE, SE, FE> From<ShardTreeError<CE>> for Error<DE, CE, SE, FE> { impl<DE, TE, SE, FE, CE, N> From<ShardTreeError<TE>> for Error<DE, TE, SE, FE, CE, N> {
fn from(e: ShardTreeError<CE>) -> Self { fn from(e: ShardTreeError<TE>) -> Self {
Error::CommitmentTree(e) Error::CommitmentTree(e)
} }
} }

View File

@ -31,7 +31,7 @@ use zcash_primitives::{
memo::Memo, memo::Memo,
transaction::{ transaction::{
components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, components::{amount::NonNegativeAmount, sapling::zip212_enforcement},
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule}, fees::{FeeRule, StandardFeeRule},
Transaction, TxId, Transaction, TxId,
}, },
}; };
@ -46,7 +46,10 @@ use zip32::{fingerprint::SeedFingerprint, DiversifierIndex};
use crate::{ use crate::{
address::UnifiedAddress, address::UnifiedAddress,
fees::{standard, DustOutputPolicy}, fees::{
standard::{self, SingleOutputChangeStrategy},
ChangeStrategy, DustOutputPolicy,
},
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
proposal::Proposal, proposal::Proposal,
proto::compact_formats::{ proto::compact_formats::{
@ -62,7 +65,7 @@ use super::{
scanning::ScanRange, scanning::ScanRange,
wallet::{ wallet::{
create_proposed_transactions, create_spend_to_address, create_proposed_transactions, create_spend_to_address,
input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector}, input_selection::{GreedyInputSelector, InputSelector},
propose_standard_transfer_to_address, propose_transfer, spend, propose_standard_transfer_to_address, propose_transfer, spend,
}, },
Account, AccountBalance, AccountBirthday, AccountPurpose, AccountSource, BlockMetadata, Account, AccountBalance, AccountBirthday, AccountPurpose, AccountSource, BlockMetadata,
@ -874,12 +877,7 @@ where
fallback_change_pool: ShieldedProtocol, fallback_change_pool: ShieldedProtocol,
) -> Result< ) -> Result<
NonEmpty<TxId>, NonEmpty<TxId>,
super::error::Error< super::wallet::TransferErrT<DbT, GreedyInputSelector<DbT>, SingleOutputChangeStrategy<DbT>>,
ErrT,
<DbT as WalletCommitmentTrees>::Error,
GreedyInputSelectorError<Zip317FeeError, <DbT as InputSource>::NoteRef>,
Zip317FeeError,
>,
> { > {
let prover = LocalTxProver::bundled(); let prover = LocalTxProver::bundled();
let network = self.network().clone(); let network = self.network().clone();
@ -901,24 +899,18 @@ where
/// Invokes [`spend`] with the given arguments. /// Invokes [`spend`] with the given arguments.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn spend<InputsT>( pub fn spend<InputsT, ChangeT>(
&mut self, &mut self,
input_selector: &InputsT, input_selector: &InputsT,
change_strategy: &ChangeT,
usk: &UnifiedSpendingKey, usk: &UnifiedSpendingKey,
request: zip321::TransactionRequest, request: zip321::TransactionRequest,
ovk_policy: OvkPolicy, ovk_policy: OvkPolicy,
min_confirmations: NonZeroU32, min_confirmations: NonZeroU32,
) -> Result< ) -> Result<NonEmpty<TxId>, super::wallet::TransferErrT<DbT, InputsT, ChangeT>>
NonEmpty<TxId>,
super::error::Error<
ErrT,
<DbT as WalletCommitmentTrees>::Error,
InputsT::Error,
<InputsT::FeeRule as FeeRule>::Error,
>,
>
where where
InputsT: InputSelector<InputSource = DbT>, InputsT: InputSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{ {
#![allow(deprecated)] #![allow(deprecated)]
let prover = LocalTxProver::bundled(); let prover = LocalTxProver::bundled();
@ -929,6 +921,7 @@ where
&prover, &prover,
&prover, &prover,
input_selector, input_selector,
change_strategy,
usk, usk,
request, request,
ovk_policy, ovk_policy,
@ -938,25 +931,28 @@ where
/// Invokes [`propose_transfer`] with the given arguments. /// Invokes [`propose_transfer`] with the given arguments.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn propose_transfer<InputsT>( pub fn propose_transfer<InputsT, ChangeT>(
&mut self, &mut self,
spend_from_account: <DbT as InputSource>::AccountId, spend_from_account: <DbT as InputSource>::AccountId,
input_selector: &InputsT, input_selector: &InputsT,
change_strategy: &ChangeT,
request: zip321::TransactionRequest, request: zip321::TransactionRequest,
min_confirmations: NonZeroU32, min_confirmations: NonZeroU32,
) -> Result< ) -> Result<
Proposal<InputsT::FeeRule, <DbT as InputSource>::NoteRef>, Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
super::error::Error<ErrT, Infallible, InputsT::Error, <InputsT::FeeRule as FeeRule>::Error>, super::wallet::ProposeTransferErrT<DbT, Infallible, InputsT, ChangeT>,
> >
where where
InputsT: InputSelector<InputSource = DbT>, InputsT: InputSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{ {
let network = self.network().clone(); let network = self.network().clone();
propose_transfer::<_, _, _, Infallible>( propose_transfer::<_, _, _, _, Infallible>(
self.wallet_mut(), self.wallet_mut(),
&network, &network,
spend_from_account, spend_from_account,
input_selector, input_selector,
change_strategy,
request, request,
min_confirmations, min_confirmations,
) )
@ -977,11 +973,11 @@ where
fallback_change_pool: ShieldedProtocol, fallback_change_pool: ShieldedProtocol,
) -> Result< ) -> Result<
Proposal<StandardFeeRule, <DbT as InputSource>::NoteRef>, Proposal<StandardFeeRule, <DbT as InputSource>::NoteRef>,
super::error::Error< super::wallet::ProposeTransferErrT<
ErrT, DbT,
CommitmentTreeErrT, CommitmentTreeErrT,
GreedyInputSelectorError<Zip317FeeError, <DbT as InputSource>::NoteRef>, GreedyInputSelector<DbT>,
Zip317FeeError, SingleOutputChangeStrategy<DbT>,
>, >,
> { > {
let network = self.network().clone(); let network = self.network().clone();
@ -1011,47 +1007,47 @@ where
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
#[allow(dead_code)] #[allow(dead_code)]
pub fn propose_shielding<InputsT>( pub fn propose_shielding<InputsT, ChangeT>(
&mut self, &mut self,
input_selector: &InputsT, input_selector: &InputsT,
change_strategy: &ChangeT,
shielding_threshold: NonNegativeAmount, shielding_threshold: NonNegativeAmount,
from_addrs: &[TransparentAddress], from_addrs: &[TransparentAddress],
to_account: <InputsT::InputSource as InputSource>::AccountId,
min_confirmations: u32, min_confirmations: u32,
) -> Result< ) -> Result<
Proposal<InputsT::FeeRule, Infallible>, Proposal<ChangeT::FeeRule, Infallible>,
super::error::Error<ErrT, Infallible, InputsT::Error, <InputsT::FeeRule as FeeRule>::Error>, super::wallet::ProposeShieldingErrT<DbT, Infallible, InputsT, ChangeT>,
> >
where where
InputsT: ShieldingSelector<InputSource = DbT>, InputsT: ShieldingSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{ {
use super::wallet::propose_shielding; use super::wallet::propose_shielding;
let network = self.network().clone(); let network = self.network().clone();
propose_shielding::<_, _, _, Infallible>( propose_shielding::<_, _, _, _, Infallible>(
self.wallet_mut(), self.wallet_mut(),
&network, &network,
input_selector, input_selector,
change_strategy,
shielding_threshold, shielding_threshold,
from_addrs, from_addrs,
to_account,
min_confirmations, min_confirmations,
) )
} }
/// Invokes [`create_proposed_transactions`] with the given arguments. /// Invokes [`create_proposed_transactions`] with the given arguments.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn create_proposed_transactions<InputsErrT, FeeRuleT>( pub fn create_proposed_transactions<InputsErrT, FeeRuleT, ChangeErrT>(
&mut self, &mut self,
usk: &UnifiedSpendingKey, usk: &UnifiedSpendingKey,
ovk_policy: OvkPolicy, ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>, proposal: &Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
) -> Result< ) -> Result<
NonEmpty<TxId>, NonEmpty<TxId>,
super::error::Error< super::wallet::CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, DbT::NoteRef>,
ErrT,
<DbT as WalletCommitmentTrees>::Error,
InputsErrT,
FeeRuleT::Error,
>,
> >
where where
FeeRuleT: FeeRule, FeeRuleT: FeeRule,
@ -1074,24 +1070,20 @@ where
/// [`shield_transparent_funds`]: crate::data_api::wallet::shield_transparent_funds /// [`shield_transparent_funds`]: crate::data_api::wallet::shield_transparent_funds
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn shield_transparent_funds<InputsT>( #[allow(clippy::too_many_arguments)]
pub fn shield_transparent_funds<InputsT, ChangeT>(
&mut self, &mut self,
input_selector: &InputsT, input_selector: &InputsT,
change_strategy: &ChangeT,
shielding_threshold: NonNegativeAmount, shielding_threshold: NonNegativeAmount,
usk: &UnifiedSpendingKey, usk: &UnifiedSpendingKey,
from_addrs: &[TransparentAddress], from_addrs: &[TransparentAddress],
to_account: <DbT as InputSource>::AccountId,
min_confirmations: u32, min_confirmations: u32,
) -> Result< ) -> Result<NonEmpty<TxId>, super::wallet::ShieldErrT<DbT, InputsT, ChangeT>>
NonEmpty<TxId>,
super::error::Error<
ErrT,
<DbT as WalletCommitmentTrees>::Error,
InputsT::Error,
<InputsT::FeeRule as FeeRule>::Error,
>,
>
where where
InputsT: ShieldingSelector<InputSource = DbT>, InputsT: ShieldingSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{ {
use crate::data_api::wallet::shield_transparent_funds; use crate::data_api::wallet::shield_transparent_funds;
@ -1103,9 +1095,11 @@ where
&prover, &prover,
&prover, &prover,
input_selector, input_selector,
change_strategy,
shielding_threshold, shielding_threshold,
usk, usk,
from_addrs, from_addrs,
to_account,
min_confirmations, min_confirmations,
) )
} }
@ -1229,15 +1223,22 @@ impl<Cache, DbT: WalletRead + Reset> TestState<Cache, DbT, LocalNetwork> {
/// Helper method for constructing a [`GreedyInputSelector`] with a /// Helper method for constructing a [`GreedyInputSelector`] with a
/// [`standard::SingleOutputChangeStrategy`]. /// [`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, fee_rule: StandardFeeRule,
change_memo: Option<&str>, change_memo: Option<&str>,
fallback_change_pool: ShieldedProtocol, 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_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap()));
let change_strategy = standard::SingleOutputChangeStrategy::new(
standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, fallback_change_pool); fee_rule,
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default()) change_memo,
fallback_change_pool,
DustOutputPolicy::default(),
)
} }
// Checks that a protobuf proposal serialized from the provided proposal value correctly parses to // 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, legacy::TransparentAddress,
transaction::{ transaction::{
components::amount::NonNegativeAmount, components::amount::NonNegativeAmount,
fees::{ fees::{fixed::FeeRule as FixedFeeRule, StandardFeeRule},
fixed::FeeRule as FixedFeeRule, zip317::FeeError as Zip317FeeError, StandardFeeRule,
},
Transaction, Transaction,
}, },
}; };
@ -38,16 +36,22 @@ use crate::{
self, self,
chain::{self, ChainState, CommitmentTreeRoot, ScanSummary}, chain::{self, ChainState, CommitmentTreeRoot, ScanSummary},
error::Error, error::Error,
testing::{input_selector, AddressType, FakeCompactOutput, InitialChainState, TestBuilder}, testing::{
single_output_change_strategy, AddressType, FakeCompactOutput, InitialChainState,
TestBuilder,
},
wallet::{ wallet::{
decrypt_and_store_transaction, decrypt_and_store_transaction, input_selection::GreedyInputSelector, TransferErrT,
input_selection::{GreedyInputSelector, GreedyInputSelectorError},
}, },
Account as _, AccountBirthday, DecryptedTransaction, InputSource, Ratio, Account as _, AccountBirthday, DecryptedTransaction, InputSource, Ratio,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletTest, WalletWrite, WalletCommitmentTrees, WalletRead, WalletSummary, WalletTest, WalletWrite,
}, },
decrypt_transaction, decrypt_transaction,
fees::{fixed, standard, DustOutputPolicy}, fees::{
fixed,
standard::{self, SingleOutputChangeStrategy},
DustOutputPolicy,
},
scanning::ScanError, scanning::ScanError,
wallet::{Note, NoteId, OvkPolicy, ReceivedNote}, wallet::{Note, NoteId, OvkPolicy, ReceivedNote},
}; };
@ -216,19 +220,21 @@ pub fn send_single_step_proposed_transfer<T: ShieldedPoolTester>(
fee_rule, fee_rule,
Some(change_memo.clone().into()), Some(change_memo.clone().into()),
T::SHIELDED_PROTOCOL, T::SHIELDED_PROTOCOL,
DustOutputPolicy::default(),
); );
let input_selector = &GreedyInputSelector::new(change_strategy, DustOutputPolicy::default()); let input_selector = GreedyInputSelector::new();
let proposal = st let proposal = st
.propose_transfer( .propose_transfer(
account.id(), account.id(),
input_selector, &input_selector,
&change_strategy,
request, request,
NonZeroU32::new(1).unwrap(), NonZeroU32::new(1).unwrap(),
) )
.unwrap(); .unwrap();
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>( let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(), account.usk(),
OvkPolicy::Sender, OvkPolicy::Sender,
&proposal, &proposal,
@ -399,7 +405,7 @@ pub fn send_multi_step_proposed_transfer<T: ShieldedPoolTester, DSF>(
); );
assert_eq!(steps[1].balance().proposed_change(), []); 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(), account.usk(),
OvkPolicy::Sender, OvkPolicy::Sender,
&proposal, &proposal,
@ -499,7 +505,7 @@ pub fn send_multi_step_proposed_transfer<T: ShieldedPoolTester, DSF>(
) )
.unwrap(); .unwrap();
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>( let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(), account.usk(),
OvkPolicy::Sender, OvkPolicy::Sender,
&proposal, &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`, // This is somewhat redundant with `send_multi_step_proposed_transfer`,
// but tests the case with no change memo and ensures we haven't messed // but tests the case with no change memo and ensures we haven't messed
// up the test setup. // 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(), account.usk(),
OvkPolicy::Sender, OvkPolicy::Sender,
&proposal, &proposal,
@ -774,7 +780,7 @@ pub fn proposal_fails_if_not_all_ephemeral_outputs_consumed<T: ShieldedPoolTeste
) )
.unwrap(); .unwrap();
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>( let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(), account.usk(),
OvkPolicy::Sender, OvkPolicy::Sender,
&frobbed_proposal, &frobbed_proposal,
@ -998,7 +1004,11 @@ pub fn spend_fails_on_unverified_notes<T: ShieldedPoolTester>(
// Executing the proposal should succeed // Executing the proposal should succeed
let txid = st let txid = st
.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal) .create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal,
)
.unwrap()[0]; .unwrap()[0];
let (h, _) = st.generate_next_block_including(txid); 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 // Executing the proposal should succeed
assert_matches!( 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 Ok(txids) if txids.len() == 1
); );
@ -1139,7 +1149,11 @@ pub fn spend_fails_on_locked_notes<T: ShieldedPoolTester>(
.unwrap(); .unwrap();
let txid2 = st let txid2 = st
.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal) .create_proposed_transactions::<Infallible, _, Infallible>(
account.usk(),
OvkPolicy::Sender,
&proposal,
)
.unwrap()[0]; .unwrap()[0];
let (h, _) = st.generate_next_block_including(txid2); 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| ovk_policy|
-> Result< -> Result<
Option<(Note, Address, MemoBytes)>, 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 min_confirmations = NonZeroU32::new(1).unwrap();
let proposal = st.propose_standard_transfer( 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 // Executing the proposal should succeed
assert_matches!( 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 Ok(txids) if txids.len() == 1
); );
} }
@ -1346,7 +1364,7 @@ pub fn change_note_spends_succeed<T: ShieldedPoolTester>(
// Executing the proposal should succeed // Executing the proposal should succeed
assert_matches!( 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 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)] #[allow(deprecated)]
let fee_rule = FixedFeeRule::standard(); let fee_rule = FixedFeeRule::standard();
let input_selector = GreedyInputSelector::new( let change_strategy = fixed::SingleOutputChangeStrategy::new(
fixed::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL), fee_rule,
None,
T::SHIELDED_PROTOCOL,
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );
let input_selector = GreedyInputSelector::new();
let txid = st let txid = st
.spend( .spend(
&input_selector, &input_selector,
&change_strategy,
&usk, &usk,
req, req,
OvkPolicy::Sender, OvkPolicy::Sender,
@ -1447,8 +1469,8 @@ pub fn external_address_change_spends_detected_in_restore_from_seed<T: ShieldedP
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn zip317_spend<T: ShieldedPoolTester>( pub fn zip317_spend<T: ShieldedPoolTester, DSF: DataStoreFactory>(
ds_factory: impl DataStoreFactory, ds_factory: DSF,
cache: impl TestCache, cache: impl TestCache,
) { ) {
let mut st = TestBuilder::new() 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_total_balance(account_id), total);
assert_eq!(st.get_spendable_balance(account_id, 1), 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 // This first request will fail due to insufficient non-dust funds
let req = TransactionRequest::new(vec![Payment::without_memo( let req = TransactionRequest::new(vec![Payment::without_memo(
@ -1496,6 +1520,7 @@ pub fn zip317_spend<T: ShieldedPoolTester>(
assert_matches!( assert_matches!(
st.spend( st.spend(
&input_selector, &input_selector,
&change_strategy,
account.usk(), account.usk(),
req, req,
OvkPolicy::Sender, OvkPolicy::Sender,
@ -1517,6 +1542,7 @@ pub fn zip317_spend<T: ShieldedPoolTester>(
let txid = st let txid = st
.spend( .spend(
&input_selector, &input_selector,
&change_strategy,
account.usk(), account.usk(),
req, req,
OvkPolicy::Sender, OvkPolicy::Sender,
@ -1579,19 +1605,18 @@ where
let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo); let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo);
assert_matches!(res0, Ok(_)); assert_matches!(res0, Ok(_));
let fee_rule = StandardFeeRule::Zip317; let input_selector = GreedyInputSelector::new();
let change_strategy =
let input_selector = GreedyInputSelector::new( single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
standard::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL),
DustOutputPolicy::default(),
);
let txids = st let txids = st
.shield_transparent_funds( .shield_transparent_funds(
&input_selector, &input_selector,
&change_strategy,
NonNegativeAmount::from_u64(10000).unwrap(), NonNegativeAmount::from_u64(10000).unwrap(),
account.usk(), account.usk(),
&[*taddr], &[*taddr],
account.id(),
1, 1,
) )
.unwrap(); .unwrap();
@ -1827,15 +1852,14 @@ pub fn pool_crossing_required<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
)]) )])
.unwrap(); .unwrap();
let fee_rule = StandardFeeRule::Zip317; let input_selector = GreedyInputSelector::new();
let input_selector = GreedyInputSelector::new( let change_strategy =
standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL), single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL);
DustOutputPolicy::default(),
);
let proposal0 = st let proposal0 = st
.propose_transfer( .propose_transfer(
account.id(), account.id(),
&input_selector, &input_selector,
&change_strategy,
p0_to_p1, p0_to_p1,
NonZeroU32::new(1).unwrap(), NonZeroU32::new(1).unwrap(),
) )
@ -1863,7 +1887,7 @@ pub fn pool_crossing_required<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
); );
assert_eq!(change_output.value(), expected_change); 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(), account.usk(),
OvkPolicy::Sender, OvkPolicy::Sender,
&proposal0, &proposal0,
@ -1918,17 +1942,16 @@ pub fn fully_funded_fully_private<P0: ShieldedPoolTester, P1: ShieldedPoolTester
)]) )])
.unwrap(); .unwrap();
let fee_rule = StandardFeeRule::Zip317; let input_selector = GreedyInputSelector::new();
let input_selector = GreedyInputSelector::new( // We set the default change output pool to P0, because we want to verify later that
// 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).
// change is actually sent to P1 (as the transaction is fully fundable from P1). let change_strategy =
standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL), single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL);
DustOutputPolicy::default(),
);
let proposal0 = st let proposal0 = st
.propose_transfer( .propose_transfer(
account.id(), account.id(),
&input_selector, &input_selector,
&change_strategy,
p0_to_p1, p0_to_p1,
NonZeroU32::new(1).unwrap(), 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); 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(), account.usk(),
OvkPolicy::Sender, OvkPolicy::Sender,
&proposal0, &proposal0,
@ -2009,17 +2032,16 @@ pub fn fully_funded_send_to_t<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
)]) )])
.unwrap(); .unwrap();
let fee_rule = StandardFeeRule::Zip317; let input_selector = GreedyInputSelector::new();
let input_selector = GreedyInputSelector::new( // We set the default change output pool to P0, because we want to verify later that
// 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).
// change is actually sent to P1 (as the transaction is fully fundable from P1). let change_strategy =
standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL), single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL);
DustOutputPolicy::default(),
);
let proposal0 = st let proposal0 = st
.propose_transfer( .propose_transfer(
account.id(), account.id(),
&input_selector, &input_selector,
&change_strategy,
p0_to_p1, p0_to_p1,
NonZeroU32::new(1).unwrap(), 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.output_pool(), PoolType::SAPLING);
assert_eq!(change_output.value(), expected_change); 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(), account.usk(),
OvkPolicy::Sender, OvkPolicy::Sender,
&proposal0, &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); 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. // 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();
let input_selector = GreedyInputSelector::new( let change_strategy =
standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL), single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL);
DustOutputPolicy::default(),
);
// First, send funds just to P0 // First, send funds just to P0
let transfer_amount = NonNegativeAmount::const_from_u64(200000); let transfer_amount = NonNegativeAmount::const_from_u64(200000);
@ -2126,6 +2146,7 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
let res = st let res = st
.spend( .spend(
&input_selector, &input_selector,
&change_strategy,
account.usk(), account.usk(),
p0_transfer, p0_transfer,
OvkPolicy::Sender, OvkPolicy::Sender,
@ -2157,6 +2178,7 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
let res = st let res = st
.spend( .spend(
&input_selector, &input_selector,
&change_strategy,
account.usk(), account.usk(),
both_transfer, both_transfer,
OvkPolicy::Sender, OvkPolicy::Sender,
@ -2597,17 +2619,14 @@ pub fn scan_cached_blocks_allows_blocks_out_of_order<T: ShieldedPoolTester>(
.unwrap(); .unwrap();
#[allow(deprecated)] #[allow(deprecated)]
let input_selector = GreedyInputSelector::new( let input_selector = GreedyInputSelector::new();
standard::SingleOutputChangeStrategy::new( let change_strategy =
StandardFeeRule::Zip317, single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
None,
T::SHIELDED_PROTOCOL,
),
DustOutputPolicy::default(),
);
assert_matches!( assert_matches!(
st.spend( st.spend(
&input_selector, &input_selector,
&change_strategy,
account.usk(), account.usk(),
req, req,
OvkPolicy::Sender, OvkPolicy::Sender,

View File

@ -197,16 +197,23 @@ where
check_balance(&st, 0, value); check_balance(&st, 0, value);
// Shield the output. // Shield the output.
let input_selector = GreedyInputSelector::new( let input_selector = GreedyInputSelector::new();
fixed::SingleOutputChangeStrategy::new( let change_strategy = fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::ZERO), FixedFeeRule::non_standard(NonNegativeAmount::ZERO),
None, None,
ShieldedProtocol::Sapling, ShieldedProtocol::Sapling,
),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );
let txid = st 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]; .unwrap()[0];
// The wallet should have zero transparent balance, because the shielding // The wallet should have zero transparent balance, because the shielding

View File

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

View File

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

View File

@ -1,18 +1,17 @@
use std::fmt; use std::fmt::{self, Debug, Display};
use zcash_primitives::{ use zcash_primitives::{
consensus::{self, BlockHeight}, consensus::{self, BlockHeight},
memo::MemoBytes, memo::MemoBytes,
transaction::{ transaction::{
components::{ components::{amount::NonNegativeAmount, OutPoint},
amount::{BalanceError, NonNegativeAmount},
OutPoint,
},
fees::{transparent, FeeRule}, fees::{transparent, FeeRule},
}, },
}; };
use zcash_protocol::{PoolType, ShieldedProtocol}; use zcash_protocol::{PoolType, ShieldedProtocol};
use crate::data_api::InputSource;
pub(crate) mod common; pub(crate) mod common;
pub mod fixed; pub mod fixed;
#[cfg(feature = "orchard")] #[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> { impl<E, N> std::error::Error for ChangeError<E, N>
fn from(err: BalanceError) -> ChangeError<BalanceError, NoteRefT> { where
ChangeError::StrategyError(err) 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 /// 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. /// by a transaction having a specified set of inputs and outputs.
pub trait ChangeStrategy { pub trait ChangeStrategy {
type FeeRule: FeeRule; type FeeRule: FeeRule + Clone;
type Error; 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 /// Returns the fee rule that this change strategy will respect when performing
/// balance computations. /// balance computations.
fn fee_rule(&self) -> &Self::FeeRule; 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 /// Computes the totals of inputs, suggested change amounts, and fees given the
/// provided inputs and outputs being used to construct a transaction. /// 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_balance`: if the transaction is to be constructed with either an
/// ephemeral transparent input or an ephemeral transparent output this argument /// 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 /// 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 /// [ZIP 320]: https://zips.z.cash/zip-0320
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -405,8 +432,8 @@ pub trait ChangeStrategy {
transparent_outputs: &[impl transparent::OutputView], transparent_outputs: &[impl transparent::OutputView],
sapling: &impl sapling::BundleView<NoteRefT>, sapling: &impl sapling::BundleView<NoteRefT>,
#[cfg(feature = "orchard")] orchard: &impl orchard::BundleView<NoteRefT>, #[cfg(feature = "orchard")] orchard: &impl orchard::BundleView<NoteRefT>,
dust_output_policy: &DustOutputPolicy,
ephemeral_balance: Option<&EphemeralBalance>, ephemeral_balance: Option<&EphemeralBalance>,
wallet_meta: Option<&Self::WalletMeta>,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>; ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>;
} }

View File

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

View File

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

View File

@ -126,7 +126,7 @@ impl Display for ProposalError {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
ProposalError::EphemeralOutputsInvalid => write!( ProposalError::EphemeralOutputsInvalid => write!(
f, 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)] #[allow(dead_code)]
pub(crate) fn zip317_spend<T: ShieldedPoolTester>() { 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, TestDbFactory,
BlockCache::new(), BlockCache::new(),
) )

View File

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

View File

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