zcash_client_backend: Add Orchard support to change strategies.

This modifies the `compute_balance` method to operate in a
bundle-oriented fashion, which simplifies the API and makes it easier to
elide Orchard functionality in the case that the `orchard` feature is
not enabled.
This commit is contained in:
Kris Nuttycombe 2023-12-04 14:45:22 -07:00
parent 0548b3dd9b
commit 56f2ac573c
13 changed files with 522 additions and 153 deletions

View File

@ -27,7 +27,8 @@ and this library adheres to Rust's notion of
- `wallet::input_selection::ShieldingSelector` has been - `wallet::input_selection::ShieldingSelector` has been
factored out from the `InputSelector` trait to separate out transparent factored out from the `InputSelector` trait to separate out transparent
functionality and move it behind the `transparent-inputs` feature flag. functionality and move it behind the `transparent-inputs` feature flag.
- `zcash_client_backend::fees::{standard, sapling}` - `zcash_client_backend::fees::{standard, orchard, sapling}`
- `zcash_client_backend::fees::ChangeValue::{new, orchard}`
- `zcash_client_backend::wallet`: - `zcash_client_backend::wallet`:
- `Note` - `Note`
- `ReceivedNote` - `ReceivedNote`
@ -141,9 +142,10 @@ and this library adheres to Rust's notion of
for its `InputSource` associated type. for its `InputSource` associated type.
- `zcash_client_backend::fees`: - `zcash_client_backend::fees`:
- `ChangeValue::Sapling` is now a structured variant. In addition to the - `ChangeStrategy::compute_balance` arguments have changed.
existing change value, it now also carries an optional memo to be associated - `ChangeValue` is now a struct. In addition to the existing change value, it
with the change output. now also provides the output pool to which change should be sent and an
optional memo to be associated with the change output.
- `fixed::SingleOutputChangeStrategy::new` and - `fixed::SingleOutputChangeStrategy::new` and
`zip317::SingleOutputChangeStrategy::new` each now accept an additional `zip317::SingleOutputChangeStrategy::new` each now accept an additional
`change_memo` argument. `change_memo` argument.
@ -157,7 +159,6 @@ and this library adheres to Rust's notion of
- `error::Error::InsufficientFunds.{available, required}` - `error::Error::InsufficientFunds.{available, required}`
- `wallet::input_selection::InputSelectorError::InsufficientFunds.{available, required}` - `wallet::input_selection::InputSelectorError::InsufficientFunds.{available, required}`
- `zcash_client_backend::fees`: - `zcash_client_backend::fees`:
- `ChangeValue::Sapling.value`
- `ChangeError::InsufficientFunds.{available, required}` - `ChangeError::InsufficientFunds.{available, required}`
- `zcash_client_backend::zip321::Payment.amount` - `zcash_client_backend::zip321::Payment.amount`
- The following methods now take `NonNegativeAmount` instead of `Amount`: - The following methods now take `NonNegativeAmount` instead of `Amount`:

View File

@ -24,7 +24,7 @@ use crate::{
SentTransactionOutput, WalletCommitmentTrees, WalletRead, WalletWrite, SentTransactionOutput, WalletCommitmentTrees, WalletRead, WalletWrite,
}, },
decrypt_transaction, decrypt_transaction,
fees::{self, ChangeValue, DustOutputPolicy}, fees::{self, DustOutputPolicy},
keys::UnifiedSpendingKey, keys::UnifiedSpendingKey,
wallet::{Note, OvkPolicy, Recipient}, wallet::{Note, OvkPolicy, Recipient},
zip321::{self, Payment}, zip321::{self, Payment},
@ -718,13 +718,15 @@ where
} }
for change_value in proposal.balance().proposed_change() { for change_value in proposal.balance().proposed_change() {
match change_value { let memo = change_value
ChangeValue::Sapling { value, memo } => { .memo()
let memo = memo.as_ref().map_or_else(MemoBytes::empty, |m| m.clone()); .map_or_else(MemoBytes::empty, |m| m.clone());
match change_value.output_pool() {
ShieldedProtocol::Sapling => {
builder.add_sapling_output( builder.add_sapling_output(
internal_ovk(), internal_ovk(),
dfvk.change_address().1, dfvk.change_address().1,
*value, change_value.value(),
memo.clone(), memo.clone(),
)?; )?;
sapling_output_meta.push(( sapling_output_meta.push((
@ -732,10 +734,19 @@ where
account, account,
PoolType::Shielded(ShieldedProtocol::Sapling), PoolType::Shielded(ShieldedProtocol::Sapling),
), ),
*value, change_value.value(),
Some(memo), Some(memo),
)) ))
} }
ShieldedProtocol::Orchard => {
#[cfg(not(feature = "orchard"))]
return Err(Error::UnsupportedPoolType(PoolType::Shielded(
ShieldedProtocol::Orchard,
)));
#[cfg(feature = "orchard")]
unimplemented!("FIXME: implement Orchard change output creation.")
}
} }
} }

View File

@ -27,10 +27,10 @@ use crate::{
}; };
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {std::collections::BTreeSet, zcash_primitives::transaction::components::OutPoint};
std::collections::BTreeSet, std::convert::Infallible,
zcash_primitives::transaction::components::OutPoint, #[cfg(feature = "orchard")]
}; use {crate::fees::orchard as orchard_fees, std::convert::Infallible};
/// The type of errors that may be produced in input selection. /// The type of errors that may be produced in input selection.
pub enum InputSelectorError<DbErrT, SelectorErrT> { pub enum InputSelectorError<DbErrT, SelectorErrT> {
@ -447,6 +447,23 @@ impl sapling::OutputView for SaplingPayment {
} }
} }
pub(crate) struct OrchardPayment(NonNegativeAmount);
// TODO: introduce this method when it is needed for testing.
// #[cfg(test)]
// impl OrchardPayment {
// pub(crate) fn new(amount: NonNegativeAmount) -> Self {
// OrchardPayment(amount)
// }
// }
#[cfg(feature = "orchard")]
impl orchard_fees::OutputView for OrchardPayment {
fn value(&self) -> NonNegativeAmount {
self.0
}
}
/// An [`InputSelector`] implementation that uses a greedy strategy to select between available /// An [`InputSelector`] implementation that uses a greedy strategy to select between available
/// notes. /// notes.
/// ///
@ -499,6 +516,7 @@ where
{ {
let mut transparent_outputs = vec![]; let mut transparent_outputs = vec![];
let mut sapling_outputs = vec![]; let mut sapling_outputs = vec![];
let mut orchard_outputs = vec![];
for payment in transaction_request.payments() { for payment in transaction_request.payments() {
let mut push_transparent = |taddr: TransparentAddress| { let mut push_transparent = |taddr: TransparentAddress| {
transparent_outputs.push(TxOut { transparent_outputs.push(TxOut {
@ -509,6 +527,9 @@ where
let mut push_sapling = || { let mut push_sapling = || {
sapling_outputs.push(SaplingPayment(payment.amount)); sapling_outputs.push(SaplingPayment(payment.amount));
}; };
let mut push_orchard = || {
orchard_outputs.push(OrchardPayment(payment.amount));
};
match &payment.recipient_address { match &payment.recipient_address {
Address::Transparent(addr) => { Address::Transparent(addr) => {
@ -518,7 +539,14 @@ where
push_sapling(); push_sapling();
} }
Address::Unified(addr) => { Address::Unified(addr) => {
if addr.sapling().is_some() { #[cfg(feature = "orchard")]
let has_orchard = addr.orchard().is_some();
#[cfg(not(feature = "orchard"))]
let has_orchard = false;
if has_orchard {
push_orchard();
} else if addr.sapling().is_some() {
push_sapling(); push_sapling();
} else if let Some(addr) = addr.transparent() { } else if let Some(addr) = addr.transparent() {
push_transparent(*addr); push_transparent(*addr);
@ -544,8 +572,17 @@ where
target_height, target_height,
&Vec::<WalletTransparentOutput>::new(), &Vec::<WalletTransparentOutput>::new(),
&transparent_outputs, &transparent_outputs,
&sapling_inputs, &(
&sapling_outputs, ::sapling::builder::BundleType::DEFAULT,
&sapling_inputs[..],
&sapling_outputs[..],
),
#[cfg(feature = "orchard")]
&(
::orchard::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..],
&orchard_outputs[..],
),
&self.dust_output_policy, &self.dust_output_policy,
); );
@ -652,8 +689,17 @@ where
target_height, target_height,
&transparent_inputs, &transparent_inputs,
&Vec::<TxOut>::new(), &Vec::<TxOut>::new(),
&Vec::<ReceivedNote<Infallible>>::new(), &(
&Vec::<SaplingPayment>::new(), ::sapling::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..],
&Vec::<Infallible>::new()[..],
),
#[cfg(feature = "orchard")]
&(
orchard::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..],
&Vec::<Infallible>::new()[..],
),
&self.dust_output_policy, &self.dust_output_policy,
); );
@ -668,8 +714,17 @@ where
target_height, target_height,
&transparent_inputs, &transparent_inputs,
&Vec::<TxOut>::new(), &Vec::<TxOut>::new(),
&Vec::<ReceivedNote<Infallible>>::new(), &(
&Vec::<SaplingPayment>::new(), ::sapling::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..],
&Vec::<Infallible>::new()[..],
),
#[cfg(feature = "orchard")]
&(
orchard::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..],
&Vec::<Infallible>::new()[..],
),
&self.dust_output_policy, &self.dust_output_policy,
)? )?
} }

View File

@ -12,38 +12,69 @@ use zcash_primitives::{
}, },
}; };
use crate::ShieldedProtocol;
pub(crate) mod common; pub(crate) mod common;
pub mod fixed; pub mod fixed;
pub mod orchard;
pub mod sapling; pub mod sapling;
pub mod standard; pub mod standard;
pub mod zip317; pub mod zip317;
/// A proposed change amount and output pool. /// A proposed change amount and output pool.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum ChangeValue { pub struct ChangeValue {
Sapling { output_pool: ShieldedProtocol,
value: NonNegativeAmount, value: NonNegativeAmount,
memo: Option<MemoBytes>, memo: Option<MemoBytes>,
},
} }
impl ChangeValue { impl ChangeValue {
/// Constructs a new change value from its constituent parts.
pub fn new(
output_pool: ShieldedProtocol,
value: NonNegativeAmount,
memo: Option<MemoBytes>,
) -> Self {
Self {
output_pool,
value,
memo,
}
}
/// Constructs a new change value that will be created as a Sapling output.
pub fn sapling(value: NonNegativeAmount, memo: Option<MemoBytes>) -> Self { pub fn sapling(value: NonNegativeAmount, memo: Option<MemoBytes>) -> Self {
Self::Sapling { value, memo } Self {
output_pool: ShieldedProtocol::Sapling,
value,
memo,
}
}
/// Constructs a new change value that will be created as an Orchard output.
#[cfg(feature = "orchard")]
pub fn orchard(value: NonNegativeAmount, memo: Option<MemoBytes>) -> Self {
Self {
output_pool: ShieldedProtocol::Orchard,
value,
memo,
}
}
/// Returns the pool to which the change output should be sent.
pub fn output_pool(&self) -> ShieldedProtocol {
self.output_pool
} }
/// Returns the value of the change output to be created, in zatoshis. /// Returns the value of the change output to be created, in zatoshis.
pub fn value(&self) -> NonNegativeAmount { pub fn value(&self) -> NonNegativeAmount {
match self { self.value
ChangeValue::Sapling { value, .. } => *value,
}
} }
/// Returns the memo to be associated with the change output. /// Returns the memo to be associated with the change output.
pub fn memo(&self) -> Option<&MemoBytes> { pub fn memo(&self) -> Option<&MemoBytes> {
match self { self.memo.as_ref()
ChangeValue::Sapling { memo, .. } => memo.as_ref(),
}
} }
} }
@ -120,6 +151,8 @@ pub enum ChangeError<E, NoteRefT> {
}, },
/// An error occurred that was specific to the change selection strategy in use. /// An error occurred that was specific to the change selection strategy in use.
StrategyError(E), StrategyError(E),
/// The proposed bundle structure would violate bundle type construction rules.
BundleError(&'static str),
} }
impl<E, NoteRefT> ChangeError<E, NoteRefT> { impl<E, NoteRefT> ChangeError<E, NoteRefT> {
@ -140,6 +173,7 @@ impl<E, NoteRefT> ChangeError<E, NoteRefT> {
sapling, sapling,
}, },
ChangeError::StrategyError(e) => ChangeError::StrategyError(f(e)), ChangeError::StrategyError(e) => ChangeError::StrategyError(f(e)),
ChangeError::BundleError(e) => ChangeError::BundleError(e),
} }
} }
} }
@ -167,6 +201,13 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for ChangeError<CE, N> {
ChangeError::StrategyError(err) => { ChangeError::StrategyError(err) => {
write!(f, "{}", err) write!(f, "{}", err)
} }
ChangeError::BundleError(err) => {
write!(
f,
"The proposed transaction structure violates bundle type constrints: {}",
err
)
}
} }
} }
} }
@ -252,8 +293,8 @@ pub trait ChangeStrategy {
target_height: BlockHeight, target_height: BlockHeight,
transparent_inputs: &[impl transparent::InputView], transparent_inputs: &[impl transparent::InputView],
transparent_outputs: &[impl transparent::OutputView], transparent_outputs: &[impl transparent::OutputView],
sapling_inputs: &[impl sapling::InputView<NoteRefT>], sapling: &impl sapling::BundleView<NoteRefT>,
sapling_outputs: &[impl sapling::OutputView], #[cfg(feature = "orchard")] orchard: &impl orchard::BundleView<NoteRefT>,
dust_output_policy: &DustOutputPolicy, dust_output_policy: &DustOutputPolicy,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>; ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>;
} }

View File

@ -7,7 +7,15 @@ use zcash_primitives::{
}, },
}; };
use super::{sapling, ChangeError, ChangeValue, DustAction, DustOutputPolicy, TransactionBalance}; use crate::ShieldedProtocol;
use super::{
sapling as sapling_fees, ChangeError, ChangeValue, DustAction, DustOutputPolicy,
TransactionBalance,
};
#[cfg(feature = "orchard")]
use super::orchard as orchard_fees;
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) fn single_change_output_balance< pub(crate) fn single_change_output_balance<
@ -21,8 +29,8 @@ pub(crate) fn single_change_output_balance<
target_height: BlockHeight, target_height: BlockHeight,
transparent_inputs: &[impl transparent::InputView], transparent_inputs: &[impl transparent::InputView],
transparent_outputs: &[impl transparent::OutputView], transparent_outputs: &[impl transparent::OutputView],
sapling_inputs: &[impl sapling::InputView<NoteRefT>], sapling: &impl sapling_fees::BundleView<NoteRefT>,
sapling_outputs: &[impl sapling::OutputView], #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
dust_output_policy: &DustOutputPolicy, dust_output_policy: &DustOutputPolicy,
default_dust_threshold: NonNegativeAmount, default_dust_threshold: NonNegativeAmount,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
@ -43,40 +51,88 @@ where
.map(|t_out| t_out.value()) .map(|t_out| t_out.value())
.sum::<Option<_>>() .sum::<Option<_>>()
.ok_or_else(overflow)?; .ok_or_else(overflow)?;
let sapling_in = sapling_inputs let sapling_in = sapling
.inputs()
.iter() .iter()
.map(|s_in| s_in.value()) .map(sapling_fees::InputView::<NoteRefT>::value)
.sum::<Option<_>>() .sum::<Option<_>>()
.ok_or_else(overflow)?; .ok_or_else(overflow)?;
let sapling_out = sapling_outputs let sapling_out = sapling
.outputs()
.iter() .iter()
.map(|s_out| s_out.value()) .map(sapling_fees::OutputView::value)
.sum::<Option<_>>() .sum::<Option<_>>()
.ok_or_else(overflow)?; .ok_or_else(overflow)?;
#[cfg(feature = "orchard")]
let orchard_in = orchard
.inputs()
.iter()
.map(orchard_fees::InputView::<NoteRefT>::value)
.sum::<Option<_>>()
.ok_or_else(overflow)?;
#[cfg(not(feature = "orchard"))]
let orchard_in = NonNegativeAmount::ZERO;
#[cfg(feature = "orchard")]
let orchard_out = orchard
.outputs()
.iter()
.map(orchard_fees::OutputView::value)
.sum::<Option<_>>()
.ok_or_else(overflow)?;
#[cfg(not(feature = "orchard"))]
let orchard_out = NonNegativeAmount::ZERO;
// TODO: implement a less naive strategy for selecting the pool to which change will be sent.
#[cfg(feature = "orchard")]
let (change_pool, sapling_change, orchard_change) =
if orchard_in > NonNegativeAmount::ZERO || orchard_out > NonNegativeAmount::ZERO {
// Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs
(ShieldedProtocol::Orchard, 0, 1)
} else if sapling_in > NonNegativeAmount::ZERO || sapling_out > NonNegativeAmount::ZERO {
// Otherwise, send change to Sapling if we're spending any Sapling inputs or creating any
// Sapling outputs, so that we avoid pool-crossing.
(ShieldedProtocol::Sapling, 1, 0)
} else {
// For all other transactions, send change to Sapling.
// FIXME: Change this to Orchard once Orchard outputs are enabled.
(ShieldedProtocol::Sapling, 1, 0)
};
#[cfg(not(feature = "orchard"))]
let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1);
#[cfg(feature = "orchard")]
let orchard_num_actions = orchard
.bundle_type()
.num_actions(
orchard.inputs().len(),
orchard.outputs().len() + orchard_change,
)
.map_err(ChangeError::BundleError)?;
#[cfg(not(feature = "orchard"))]
let orchard_num_actions = 0;
let fee_amount = fee_rule let fee_amount = fee_rule
.fee_required( .fee_required(
params, params,
target_height, target_height,
transparent_inputs, transparent_inputs,
transparent_outputs, transparent_outputs,
sapling_inputs.len(), sapling.inputs().len(),
if sapling_inputs.is_empty() { sapling
sapling_outputs.len() + 1 .bundle_type()
} else { .num_outputs(
std::cmp::max(sapling_outputs.len() + 1, 2) sapling.inputs().len(),
}, sapling.outputs().len() + sapling_change,
//Orchard is not yet supported in zcash_client_backend )
0, .map_err(ChangeError::BundleError)?,
orchard_num_actions,
) )
.map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?; .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?;
let total_in = (t_in + sapling_in).ok_or_else(overflow)?; let total_in = (t_in + sapling_in + orchard_in).ok_or_else(overflow)?;
let total_out = (t_out + sapling_out + orchard_out + fee_amount).ok_or_else(overflow)?;
let total_out = [t_out, sapling_out, fee_amount]
.iter()
.sum::<Option<NonNegativeAmount>>()
.ok_or_else(overflow)?;
let proposed_change = (total_in - total_out).ok_or(ChangeError::InsufficientFunds { let proposed_change = (total_in - total_out).ok_or(ChangeError::InsufficientFunds {
available: total_in, available: total_in,
@ -101,7 +157,7 @@ where
}) })
} }
DustAction::AllowDustChange => TransactionBalance::new( DustAction::AllowDustChange => TransactionBalance::new(
vec![ChangeValue::sapling(proposed_change, change_memo)], vec![ChangeValue::new(change_pool, proposed_change, change_memo)],
fee_amount, fee_amount,
) )
.map_err(|_| overflow()), .map_err(|_| overflow()),
@ -113,7 +169,7 @@ where
} }
} else { } else {
TransactionBalance::new( TransactionBalance::new(
vec![ChangeValue::sapling(proposed_change, change_memo)], vec![ChangeValue::new(change_pool, proposed_change, change_memo)],
fee_amount, fee_amount,
) )
.map_err(|_| overflow()) .map_err(|_| overflow())

View File

@ -10,10 +10,13 @@ use zcash_primitives::{
}; };
use super::{ use super::{
common::single_change_output_balance, sapling, ChangeError, ChangeStrategy, DustOutputPolicy, common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
TransactionBalance, DustOutputPolicy, TransactionBalance,
}; };
#[cfg(feature = "orchard")]
use super::orchard as orchard_fees;
/// A change strategy that and proposes change as a single output to the most current supported /// A change strategy that and proposes change as a single output to the most current supported
/// shielded pool and delegates fee calculation to the provided fee rule. /// shielded pool and delegates fee calculation to the provided fee rule.
pub struct SingleOutputChangeStrategy { pub struct SingleOutputChangeStrategy {
@ -46,8 +49,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
target_height: BlockHeight, target_height: BlockHeight,
transparent_inputs: &[impl transparent::InputView], transparent_inputs: &[impl transparent::InputView],
transparent_outputs: &[impl transparent::OutputView], transparent_outputs: &[impl transparent::OutputView],
sapling_inputs: &[impl sapling::InputView<NoteRefT>], sapling: &impl sapling_fees::BundleView<NoteRefT>,
sapling_outputs: &[impl sapling::OutputView], #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
dust_output_policy: &DustOutputPolicy, dust_output_policy: &DustOutputPolicy,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> { ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
single_change_output_balance( single_change_output_balance(
@ -56,8 +59,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
target_height, target_height,
transparent_inputs, transparent_inputs,
transparent_outputs, transparent_outputs,
sapling_inputs, sapling,
sapling_outputs, #[cfg(feature = "orchard")]
orchard,
dust_output_policy, dust_output_policy,
self.fee_rule().fixed_fee(), self.fee_rule().fixed_fee(),
self.change_memo.clone(), self.change_memo.clone(),
@ -67,6 +71,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[cfg(feature = "orchard")]
use std::convert::Infallible;
use zcash_primitives::{ use zcash_primitives::{
consensus::{Network, NetworkUpgrade, Parameters}, consensus::{Network, NetworkUpgrade, Parameters},
transaction::{ transaction::{
@ -98,13 +105,22 @@ mod tests {
.unwrap(), .unwrap(),
&Vec::<TestTransparentInput>::new(), &Vec::<TestTransparentInput>::new(),
&Vec::<TxOut>::new(), &Vec::<TxOut>::new(),
&[TestSaplingInput { &(
note_id: 0, sapling::builder::BundleType::DEFAULT,
value: NonNegativeAmount::const_from_u64(60000), &[TestSaplingInput {
}], note_id: 0,
&[SaplingPayment::new(NonNegativeAmount::const_from_u64( value: NonNegativeAmount::const_from_u64(60000),
40000, }][..],
))], &[SaplingPayment::new(NonNegativeAmount::const_from_u64(
40000,
))][..],
),
#[cfg(feature = "orchard")]
&(
orchard::builder::BundleType::DEFAULT,
&[] as &[Infallible],
&[] as &[Infallible],
),
&DustOutputPolicy::default(), &DustOutputPolicy::default(),
); );
@ -130,20 +146,29 @@ mod tests {
.unwrap(), .unwrap(),
&Vec::<TestTransparentInput>::new(), &Vec::<TestTransparentInput>::new(),
&Vec::<TxOut>::new(), &Vec::<TxOut>::new(),
&[ &(
TestSaplingInput { sapling::builder::BundleType::DEFAULT,
note_id: 0, &[
value: NonNegativeAmount::const_from_u64(40000), TestSaplingInput {
}, note_id: 0,
// enough to pay a fee, plus dust value: NonNegativeAmount::const_from_u64(40000),
TestSaplingInput { },
note_id: 0, // enough to pay a fee, plus dust
value: NonNegativeAmount::const_from_u64(10100), TestSaplingInput {
}, note_id: 0,
], value: NonNegativeAmount::const_from_u64(10100),
&[SaplingPayment::new(NonNegativeAmount::const_from_u64( },
40000, ][..],
))], &[SaplingPayment::new(NonNegativeAmount::const_from_u64(
40000,
))][..],
),
#[cfg(feature = "orchard")]
&(
orchard::builder::BundleType::DEFAULT,
&[] as &[Infallible],
&[] as &[Infallible],
),
&DustOutputPolicy::default(), &DustOutputPolicy::default(),
); );

View File

@ -0,0 +1,76 @@
//! Types related to computation of fees and change related to the Orchard components
//! of a transaction.
use std::convert::Infallible;
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
#[cfg(feature = "orchard")]
use orchard::builder::BundleType;
/// A trait that provides a minimized view of Orchard bundle configuration
/// suitable for use in fee and change calculation.
#[cfg(feature = "orchard")]
pub trait BundleView<NoteRef> {
/// The type of inputs to the bundle.
type In: InputView<NoteRef>;
/// The type of inputs of the bundle.
type Out: OutputView;
/// Returns the type of the bundle
fn bundle_type(&self) -> BundleType;
/// Returns the inputs to the bundle.
fn inputs(&self) -> &[Self::In];
/// Returns the outputs of the bundle.
fn outputs(&self) -> &[Self::Out];
}
#[cfg(feature = "orchard")]
impl<'a, NoteRef, In: InputView<NoteRef>, Out: OutputView> BundleView<NoteRef>
for (BundleType, &'a [In], &'a [Out])
{
type In = In;
type Out = Out;
fn bundle_type(&self) -> BundleType {
self.0
}
fn inputs(&self) -> &[In] {
self.1
}
fn outputs(&self) -> &[Out] {
self.2
}
}
/// A trait that provides a minimized view of an Orchard input suitable for use in fee and change
/// calculation.
pub trait InputView<NoteRef> {
/// An identifier for the input being spent.
fn note_id(&self) -> &NoteRef;
/// The value of the input being spent.
fn value(&self) -> NonNegativeAmount;
}
impl<N> InputView<N> for Infallible {
fn note_id(&self) -> &N {
unreachable!()
}
fn value(&self) -> NonNegativeAmount {
unreachable!()
}
}
/// A trait that provides a minimized view of a Orchard output suitable for use in fee and change
/// calculation.
pub trait OutputView {
/// The value of the output being produced.
fn value(&self) -> NonNegativeAmount;
}
impl OutputView for Infallible {
fn value(&self) -> NonNegativeAmount {
unreachable!()
}
}

View File

@ -3,9 +3,44 @@
use std::convert::Infallible; use std::convert::Infallible;
use sapling::builder::{OutputInfo, SpendInfo}; use sapling::builder::{BundleType, OutputInfo, SpendInfo};
use zcash_primitives::transaction::components::amount::NonNegativeAmount; use zcash_primitives::transaction::components::amount::NonNegativeAmount;
/// A trait that provides a minimized view of Sapling bundle configuration
/// suitable for use in fee and change calculation.
pub trait BundleView<NoteRef> {
/// The type of inputs to the bundle.
type In: InputView<NoteRef>;
/// The type of inputs of the bundle.
type Out: OutputView;
/// Returns the type of the bundle
fn bundle_type(&self) -> BundleType;
/// Returns the inputs to the bundle.
fn inputs(&self) -> &[Self::In];
/// Returns the outputs of the bundle.
fn outputs(&self) -> &[Self::Out];
}
impl<'a, NoteRef, In: InputView<NoteRef>, Out: OutputView> BundleView<NoteRef>
for (BundleType, &'a [In], &'a [Out])
{
type In = In;
type Out = Out;
fn bundle_type(&self) -> BundleType {
self.0
}
fn inputs(&self) -> &[In] {
self.1
}
fn outputs(&self) -> &[Out] {
self.2
}
}
/// A trait that provides a minimized view of a Sapling input suitable for use in /// A trait that provides a minimized view of a Sapling input suitable for use in
/// fee and change calculation. /// fee and change calculation.
pub trait InputView<NoteRef> { pub trait InputView<NoteRef> {

View File

@ -15,9 +15,13 @@ use zcash_primitives::{
}; };
use super::{ use super::{
fixed, sapling, zip317, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance, fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy,
TransactionBalance,
}; };
#[cfg(feature = "orchard")]
use super::orchard as orchard_fees;
/// A change strategy that proposes change as a single output to the most current supported /// A change strategy that proposes change as a single output to the most current supported
/// shielded pool and delegates fee calculation to the provided fee rule. /// shielded pool and delegates fee calculation to the provided fee rule.
pub struct SingleOutputChangeStrategy { pub struct SingleOutputChangeStrategy {
@ -50,8 +54,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
target_height: BlockHeight, target_height: BlockHeight,
transparent_inputs: &[impl transparent::InputView], transparent_inputs: &[impl transparent::InputView],
transparent_outputs: &[impl transparent::OutputView], transparent_outputs: &[impl transparent::OutputView],
sapling_inputs: &[impl sapling::InputView<NoteRefT>], sapling: &impl sapling_fees::BundleView<NoteRefT>,
sapling_outputs: &[impl sapling::OutputView], #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
dust_output_policy: &DustOutputPolicy, dust_output_policy: &DustOutputPolicy,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> { ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
#[allow(deprecated)] #[allow(deprecated)]
@ -65,8 +69,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
target_height, target_height,
transparent_inputs, transparent_inputs,
transparent_outputs, transparent_outputs,
sapling_inputs, sapling,
sapling_outputs, #[cfg(feature = "orchard")]
orchard,
dust_output_policy, dust_output_policy,
) )
.map_err(|e| e.map(Zip317FeeError::Balance)), .map_err(|e| e.map(Zip317FeeError::Balance)),
@ -79,8 +84,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
target_height, target_height,
transparent_inputs, transparent_inputs,
transparent_outputs, transparent_outputs,
sapling_inputs, sapling,
sapling_outputs, #[cfg(feature = "orchard")]
orchard,
dust_output_policy, dust_output_policy,
) )
.map_err(|e| e.map(Zip317FeeError::Balance)), .map_err(|e| e.map(Zip317FeeError::Balance)),
@ -93,8 +99,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
target_height, target_height,
transparent_inputs, transparent_inputs,
transparent_outputs, transparent_outputs,
sapling_inputs, sapling,
sapling_outputs, #[cfg(feature = "orchard")]
orchard,
dust_output_policy, dust_output_policy,
), ),
} }

View File

@ -14,10 +14,13 @@ use zcash_primitives::{
}; };
use super::{ use super::{
common::single_change_output_balance, sapling, ChangeError, ChangeStrategy, DustOutputPolicy, common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
TransactionBalance, DustOutputPolicy, TransactionBalance,
}; };
#[cfg(feature = "orchard")]
use super::orchard as orchard_fees;
/// A change strategy that and proposes change as a single output to the most current supported /// A change strategy that and proposes change as a single output to the most current supported
/// shielded pool and delegates fee calculation to the provided fee rule. /// shielded pool and delegates fee calculation to the provided fee rule.
pub struct SingleOutputChangeStrategy { pub struct SingleOutputChangeStrategy {
@ -50,8 +53,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
target_height: BlockHeight, target_height: BlockHeight,
transparent_inputs: &[impl transparent::InputView], transparent_inputs: &[impl transparent::InputView],
transparent_outputs: &[impl transparent::OutputView], transparent_outputs: &[impl transparent::OutputView],
sapling_inputs: &[impl sapling::InputView<NoteRefT>], sapling: &impl sapling_fees::BundleView<NoteRefT>,
sapling_outputs: &[impl sapling::OutputView], #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
dust_output_policy: &DustOutputPolicy, dust_output_policy: &DustOutputPolicy,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> { ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
let mut transparent_dust: Vec<_> = transparent_inputs let mut transparent_dust: Vec<_> = transparent_inputs
@ -67,11 +70,12 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
}) })
.collect(); .collect();
let mut sapling_dust: Vec<_> = sapling_inputs let mut sapling_dust: Vec<_> = sapling
.inputs()
.iter() .iter()
.filter_map(|i| { .filter_map(|i| {
if i.value() < self.fee_rule.marginal_fee() { if sapling_fees::InputView::<NoteRefT>::value(i) < self.fee_rule.marginal_fee() {
Some(i.note_id().clone()) Some(sapling_fees::InputView::<NoteRefT>::note_id(i).clone())
} else { } else {
None None
} }
@ -88,8 +92,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
// We add one to the sapling outputs for the (single) change output Note that this // We add one to the sapling outputs for the (single) change output Note that this
// means that wallet-internal shielding transactions are an opportunity to spend a dust // means that wallet-internal shielding transactions are an opportunity to spend a dust
// note. // note.
let s_non_dust = sapling_inputs.len() - sapling_dust.len(); let s_non_dust = sapling.inputs().len() - sapling_dust.len();
let s_allowed_dust = (sapling_outputs.len() + 1).saturating_sub(s_non_dust); let s_allowed_dust = (sapling.outputs().len() + 1).saturating_sub(s_non_dust);
let available_grace_inputs = self let available_grace_inputs = self
.fee_rule .fee_rule
@ -135,8 +139,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
target_height, target_height,
transparent_inputs, transparent_inputs,
transparent_outputs, transparent_outputs,
sapling_inputs, sapling,
sapling_outputs, #[cfg(feature = "orchard")]
orchard,
dust_output_policy, dust_output_policy,
self.fee_rule.marginal_fee(), self.fee_rule.marginal_fee(),
self.change_memo.clone(), self.change_memo.clone(),
@ -147,6 +152,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::convert::Infallible;
use zcash_primitives::{ use zcash_primitives::{
consensus::{Network, NetworkUpgrade, Parameters}, consensus::{Network, NetworkUpgrade, Parameters},
legacy::Script, legacy::Script,
@ -177,13 +184,22 @@ mod tests {
.unwrap(), .unwrap(),
&Vec::<TestTransparentInput>::new(), &Vec::<TestTransparentInput>::new(),
&Vec::<TxOut>::new(), &Vec::<TxOut>::new(),
&[TestSaplingInput { &(
note_id: 0, sapling::builder::BundleType::DEFAULT,
value: NonNegativeAmount::const_from_u64(55000), &[TestSaplingInput {
}], note_id: 0,
&[SaplingPayment::new(NonNegativeAmount::const_from_u64( value: NonNegativeAmount::const_from_u64(55000),
40000, }][..],
))], &[SaplingPayment::new(NonNegativeAmount::const_from_u64(
40000,
))][..],
),
#[cfg(feature = "orchard")]
&(
orchard::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..],
&Vec::<Infallible>::new()[..],
),
&DustOutputPolicy::default(), &DustOutputPolicy::default(),
); );
@ -210,11 +226,20 @@ mod tests {
value: NonNegativeAmount::const_from_u64(40000), value: NonNegativeAmount::const_from_u64(40000),
script_pubkey: Script(vec![]), script_pubkey: Script(vec![]),
}], }],
&[TestSaplingInput { &(
note_id: 0, sapling::builder::BundleType::DEFAULT,
value: NonNegativeAmount::const_from_u64(55000), &[TestSaplingInput {
}], note_id: 0,
&Vec::<SaplingPayment>::new(), value: NonNegativeAmount::const_from_u64(55000),
}][..],
&Vec::<Infallible>::new()[..],
),
#[cfg(feature = "orchard")]
&(
orchard::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..],
&Vec::<Infallible>::new()[..],
),
&DustOutputPolicy::default(), &DustOutputPolicy::default(),
); );
@ -237,19 +262,28 @@ mod tests {
.unwrap(), .unwrap(),
&Vec::<TestTransparentInput>::new(), &Vec::<TestTransparentInput>::new(),
&Vec::<TxOut>::new(), &Vec::<TxOut>::new(),
&[ &(
TestSaplingInput { sapling::builder::BundleType::DEFAULT,
note_id: 0, &[
value: NonNegativeAmount::const_from_u64(49000), TestSaplingInput {
}, note_id: 0,
TestSaplingInput { value: NonNegativeAmount::const_from_u64(49000),
note_id: 1, },
value: NonNegativeAmount::const_from_u64(1000), TestSaplingInput {
}, note_id: 1,
], value: NonNegativeAmount::const_from_u64(1000),
&[SaplingPayment::new(NonNegativeAmount::const_from_u64( },
40000, ][..],
))], &[SaplingPayment::new(NonNegativeAmount::const_from_u64(
40000,
))][..],
),
#[cfg(feature = "orchard")]
&(
orchard::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..],
&Vec::<Infallible>::new()[..],
),
&DustOutputPolicy::default(), &DustOutputPolicy::default(),
); );
@ -272,23 +306,32 @@ mod tests {
.unwrap(), .unwrap(),
&Vec::<TestTransparentInput>::new(), &Vec::<TestTransparentInput>::new(),
&Vec::<TxOut>::new(), &Vec::<TxOut>::new(),
&[ &(
TestSaplingInput { sapling::builder::BundleType::DEFAULT,
note_id: 0, &[
value: NonNegativeAmount::const_from_u64(29000), TestSaplingInput {
}, note_id: 0,
TestSaplingInput { value: NonNegativeAmount::const_from_u64(29000),
note_id: 1, },
value: NonNegativeAmount::const_from_u64(20000), TestSaplingInput {
}, note_id: 1,
TestSaplingInput { value: NonNegativeAmount::const_from_u64(20000),
note_id: 2, },
value: NonNegativeAmount::const_from_u64(1000), TestSaplingInput {
}, note_id: 2,
], value: NonNegativeAmount::const_from_u64(1000),
&[SaplingPayment::new(NonNegativeAmount::const_from_u64( },
40000, ][..],
))], &[SaplingPayment::new(NonNegativeAmount::const_from_u64(
40000,
))][..],
),
#[cfg(feature = "orchard")]
&(
orchard::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..],
&Vec::<Infallible>::new()[..],
),
&DustOutputPolicy::default(), &DustOutputPolicy::default(),
); );

View File

@ -341,17 +341,20 @@ impl proposal::Proposal {
.balance() .balance()
.proposed_change() .proposed_change()
.iter() .iter()
.map(|change| match change { .map(|change| match change.output_pool() {
ChangeValue::Sapling { value, memo } => proposal::ChangeValue { ShieldedProtocol::Sapling => proposal::ChangeValue {
value: Some(proposal::change_value::Value::SaplingValue( value: Some(proposal::change_value::Value::SaplingValue(
proposal::SaplingChange { proposal::SaplingChange {
amount: (*value).into(), amount: change.value().into(),
memo: memo.as_ref().map(|memo_bytes| proposal::MemoBytes { memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
value: memo_bytes.as_slice().to_vec(), value: memo_bytes.as_slice().to_vec(),
}), }),
}, },
)), )),
}, },
ShieldedProtocol::Orchard => {
unimplemented!("FIXME: implement Orchard change outputs!")
}
}) })
.collect(), .collect(),
fee_required: value.balance().fee_required().into(), fee_required: value.balance().fee_required().into(),

View File

@ -44,9 +44,11 @@ and this library adheres to Rust's notion of
- `impl From<&NonNegativeAmount> for Amount` - `impl From<&NonNegativeAmount> for Amount`
- `impl From<NonNegativeAmount> for u64` - `impl From<NonNegativeAmount> for u64`
- `impl From<NonNegativeAmount> for zcash_primitives::sapling::value::NoteValue` - `impl From<NonNegativeAmount> for zcash_primitives::sapling::value::NoteValue`
- `impl From<NonNegativeAmount> for orchard::::NoteValue`
- `impl Sum<NonNegativeAmount> for Option<NonNegativeAmount>` - `impl Sum<NonNegativeAmount> for Option<NonNegativeAmount>`
- `impl<'a> Sum<&'a NonNegativeAmount> for Option<NonNegativeAmount>` - `impl<'a> Sum<&'a NonNegativeAmount> for Option<NonNegativeAmount>`
- `impl TryFrom<sapling::value::NoteValue> for NonNegativeAmount` - `impl TryFrom<sapling::value::NoteValue> for NonNegativeAmount`
- `impl TryFrom<orchard::NoteValue> for NonNegativeAmount`
- `impl {Clone, PartialEq, Eq} for zcash_primitives::memo::Error` - `impl {Clone, PartialEq, Eq} for zcash_primitives::memo::Error`
- `impl {PartialEq, Eq} for zcash_primitives::sapling::note::Rseed` - `impl {PartialEq, Eq} for zcash_primitives::sapling::note::Rseed`
- `impl From<TxId> for [u8; 32]` - `impl From<TxId> for [u8; 32]`

View File

@ -331,6 +331,20 @@ impl TryFrom<sapling::value::NoteValue> for NonNegativeAmount {
} }
} }
impl From<NonNegativeAmount> for orchard::NoteValue {
fn from(n: NonNegativeAmount) -> Self {
orchard::NoteValue::from_raw(n.into())
}
}
impl TryFrom<orchard::NoteValue> for NonNegativeAmount {
type Error = ();
fn try_from(value: orchard::NoteValue) -> Result<Self, Self::Error> {
Self::from_u64(value.inner())
}
}
impl TryFrom<Amount> for NonNegativeAmount { impl TryFrom<Amount> for NonNegativeAmount {
type Error = (); type Error = ();