use std::{
convert::Infallible,
fmt::{self, Debug, Display},
num::{NonZeroU64, NonZeroUsize},
};
use ::transparent::bundle::OutPoint;
use zcash_primitives::transaction::fees::{
FeeRule,
transparent::{self, InputSize},
zip317 as prim_zip317,
};
use zcash_protocol::{
PoolType, ShieldedPool,
consensus::{self, BlockHeight},
memo::MemoBytes,
value::{BalanceError, Zatoshis},
};
use crate::data_api::{InputSource, anchor_retention::PoolMigrationParams, wallet::TargetHeight};
pub mod common;
#[cfg(feature = "non-standard-fees")]
pub mod fixed;
#[cfg(feature = "orchard")]
pub mod orchard;
pub mod sapling;
pub mod standard;
pub mod zip317;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum StandardFeeRule {
Zip317,
}
impl FeeRule for StandardFeeRule {
type Error = prim_zip317::FeeError;
fn fee_required<P: consensus::Parameters>(
&self,
params: &P,
target_height: BlockHeight,
transparent_input_sizes: impl IntoIterator<Item = InputSize>,
transparent_output_sizes: impl IntoIterator<Item = usize>,
sapling_input_count: usize,
sapling_output_count: usize,
orchard_action_count: usize,
ironwood_action_count: usize,
) -> Result<Zatoshis, Self::Error> {
#[allow(deprecated)]
match self {
Self::Zip317 => prim_zip317::FeeRule::standard().fee_required(
params,
target_height,
transparent_input_sizes,
transparent_output_sizes,
sapling_input_count,
sapling_output_count,
orchard_action_count,
ironwood_action_count,
),
}
}
}
#[cfg(feature = "transparent-inputs")]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TransparentChangePolicy {
#[default]
ShieldChange,
TransparentChangeAllowed,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChangeValue(ChangeValueInner);
#[derive(Clone, Debug, PartialEq, Eq)]
enum ChangeValueInner {
Shielded {
protocol: ShieldedPool,
value: Zatoshis,
memo: Option<MemoBytes>,
},
#[cfg(feature = "transparent-inputs")]
EphemeralTransparent { value: Zatoshis },
#[cfg(feature = "transparent-inputs")]
Transparent { value: Zatoshis },
}
impl ChangeValue {
#[cfg(feature = "transparent-inputs")]
pub fn ephemeral_transparent(value: Zatoshis) -> Self {
Self(ChangeValueInner::EphemeralTransparent { value })
}
#[cfg(feature = "transparent-inputs")]
pub fn transparent(value: Zatoshis) -> Self {
Self(ChangeValueInner::Transparent { value })
}
pub fn shielded(protocol: ShieldedPool, value: Zatoshis, memo: Option<MemoBytes>) -> Self {
Self(ChangeValueInner::Shielded {
protocol,
value,
memo,
})
}
pub fn sapling(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
Self::shielded(ShieldedPool::Sapling, value, memo)
}
#[cfg(feature = "orchard")]
pub fn orchard(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
Self::shielded(ShieldedPool::Orchard, value, memo)
}
#[cfg(feature = "orchard")]
pub fn ironwood(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
Self::shielded(ShieldedPool::Ironwood, value, memo)
}
pub fn output_pool(&self) -> PoolType {
match &self.0 {
ChangeValueInner::Shielded { protocol, .. } => PoolType::Shielded(*protocol),
#[cfg(feature = "transparent-inputs")]
ChangeValueInner::EphemeralTransparent { .. } => PoolType::Transparent,
#[cfg(feature = "transparent-inputs")]
ChangeValueInner::Transparent { .. } => PoolType::Transparent,
}
}
pub fn value(&self) -> Zatoshis {
match &self.0 {
ChangeValueInner::Shielded { value, .. } => *value,
#[cfg(feature = "transparent-inputs")]
ChangeValueInner::EphemeralTransparent { value } => *value,
#[cfg(feature = "transparent-inputs")]
ChangeValueInner::Transparent { value } => *value,
}
}
pub fn memo(&self) -> Option<&MemoBytes> {
match &self.0 {
ChangeValueInner::Shielded { memo, .. } => memo.as_ref(),
#[cfg(feature = "transparent-inputs")]
ChangeValueInner::EphemeralTransparent { .. } => None,
#[cfg(feature = "transparent-inputs")]
ChangeValueInner::Transparent { .. } => None,
}
}
#[cfg_attr(
not(feature = "transparent-inputs"),
doc = "This is always false because the `transparent-inputs` feature is
not enabled."
)]
pub fn is_ephemeral(&self) -> bool {
match &self.0 {
ChangeValueInner::Shielded { .. } => false,
#[cfg(feature = "transparent-inputs")]
ChangeValueInner::EphemeralTransparent { .. } => true,
#[cfg(feature = "transparent-inputs")]
ChangeValueInner::Transparent { .. } => false,
}
}
}
#[cfg(feature = "orchard")]
const CANONICAL_CROSSING_ORCHARD_ACTIONS: usize = 2;
#[cfg(feature = "orchard")]
const CANONICAL_CROSSING_IRONWOOD_ACTIONS: usize = 1;
#[cfg(feature = "orchard")]
pub fn canonical_crossing_fee<P: consensus::Parameters>(
params: &P,
target_height: BlockHeight,
) -> Result<Zatoshis, zcash_primitives::transaction::fees::zip317::FeeError> {
prim_zip317::FeeRule::standard().fee_required(
params,
target_height,
std::iter::empty::<InputSize>(),
std::iter::empty::<usize>(),
0,
0,
CANONICAL_CROSSING_ORCHARD_ACTIONS,
CANONICAL_CROSSING_IRONWOOD_ACTIONS,
)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TransactionBalance {
proposed_change: Vec<ChangeValue>,
fee_required: Zatoshis,
total: Zatoshis,
dummy_outputs: Option<DummyOutputCounts>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DummyOutputCounts {
sapling: usize,
#[cfg(feature = "orchard")]
orchard: usize,
#[cfg(feature = "orchard")]
ironwood: usize,
}
impl DummyOutputCounts {
pub fn new(
sapling: usize,
#[cfg(feature = "orchard")] orchard: usize,
#[cfg(feature = "orchard")] ironwood: usize,
) -> Self {
Self {
sapling,
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "orchard")]
ironwood,
}
}
pub fn sapling(&self) -> usize {
self.sapling
}
#[cfg(feature = "orchard")]
pub fn orchard(&self) -> usize {
self.orchard
}
#[cfg(feature = "orchard")]
pub fn ironwood(&self) -> usize {
self.ironwood
}
}
impl TransactionBalance {
pub fn new(
proposed_change: Vec<ChangeValue>,
fee_required: Zatoshis,
) -> Result<Self, BalanceError> {
let total = proposed_change
.iter()
.map(|c| c.value())
.chain(Some(fee_required))
.sum::<Option<Zatoshis>>()
.ok_or(BalanceError::Overflow)?;
Ok(Self {
proposed_change,
fee_required,
total,
dummy_outputs: None,
})
}
pub fn with_dummy_outputs(mut self, dummy_outputs: DummyOutputCounts) -> Self {
self.dummy_outputs = Some(dummy_outputs);
self
}
pub fn dummy_outputs(&self) -> Option<DummyOutputCounts> {
self.dummy_outputs
}
pub fn proposed_change(&self) -> &[ChangeValue] {
&self.proposed_change
}
pub fn fee_required(&self) -> Zatoshis {
self.fee_required
}
pub fn total(&self) -> Zatoshis {
self.total
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ChangeError<E, NoteRefT> {
InsufficientFunds {
available: Zatoshis,
required: Zatoshis,
},
DustInputs {
transparent: Vec<OutPoint>,
sapling: Vec<NoteRefT>,
#[cfg(feature = "orchard")]
orchard: Vec<NoteRefT>,
#[cfg(feature = "orchard")]
ironwood: Vec<NoteRefT>,
},
StrategyError(E),
BundleError(&'static str),
}
impl<CE: fmt::Display, N: fmt::Display> fmt::Display for ChangeError<CE, N> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
ChangeError::InsufficientFunds {
available,
required,
} => write!(
f,
"Insufficient funds: required {} zatoshis, but only {} zatoshis were available.",
u64::from(*required),
u64::from(*available)
),
ChangeError::DustInputs {
transparent,
sapling,
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "orchard")]
ironwood,
} => {
#[cfg(feature = "orchard")]
let orchard_len = orchard.len() + ironwood.len();
#[cfg(not(feature = "orchard"))]
let orchard_len = 0;
write!(
f,
"Insufficient funds: {} dust inputs were present, but would cost more to spend than they are worth.",
transparent.len() + sapling.len() + orchard_len,
)
}
ChangeError::StrategyError(err) => {
write!(f, "{err}")
}
ChangeError::BundleError(err) => {
write!(
f,
"The proposed transaction structure violates bundle type constraints: {err}"
)
}
}
}
}
impl<E, N> std::error::Error for ChangeError<E, N>
where
E: Debug + Display + std::error::Error + 'static,
N: Debug + Display + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self {
ChangeError::StrategyError(e) => Some(e),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DustAction {
Reject,
AllowDustChange,
AddDustToFee,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DustOutputPolicy {
action: DustAction,
dust_threshold: Option<Zatoshis>,
}
impl DustOutputPolicy {
pub fn new(action: DustAction, dust_threshold: Option<Zatoshis>) -> Self {
Self {
action,
dust_threshold,
}
}
pub fn action(&self) -> DustAction {
self.action
}
pub fn dust_threshold(&self) -> Option<Zatoshis> {
self.dust_threshold
}
}
impl Default for DustOutputPolicy {
fn default() -> Self {
DustOutputPolicy::new(DustAction::Reject, None)
}
}
#[derive(Clone, Copy, Debug)]
pub struct SplitPolicy {
target_output_count: NonZeroUsize,
min_split_output_value: Option<Zatoshis>,
}
impl SplitPolicy {
pub(crate) const MIN_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(500000);
pub fn with_min_output_value(
target_output_count: NonZeroUsize,
min_split_output_value: Zatoshis,
) -> Self {
Self {
target_output_count,
min_split_output_value: Some(min_split_output_value),
}
}
pub fn single_output() -> Self {
Self {
target_output_count: NonZeroUsize::MIN,
min_split_output_value: None,
}
}
pub fn target_output_count(&self) -> NonZeroUsize {
self.target_output_count
}
pub fn min_split_output_value(&self) -> Option<Zatoshis> {
self.min_split_output_value
}
pub fn split_count(
&self,
existing_notes: Option<usize>,
existing_notes_total: Option<Zatoshis>,
total_change: Zatoshis,
) -> NonZeroUsize {
fn to_nonzero_u64(value: usize) -> NonZeroU64 {
NonZeroU64::new(u64::try_from(value).expect("usize fits into u64"))
.expect("NonZeroU64 input derived from NonZeroUsize")
}
let mut split_count = NonZeroUsize::new(
usize::from(self.target_output_count)
.saturating_sub(existing_notes.unwrap_or(usize::MAX)),
)
.unwrap_or(NonZeroUsize::MIN);
let min_split_output_value = self.min_split_output_value.or_else(|| {
(existing_notes_total + total_change).map(|total| {
*total
.div_with_remainder(to_nonzero_u64(
usize::from(self.target_output_count).saturating_mul(4),
))
.quotient()
})
});
if let Some(min_split_output_value) = min_split_output_value {
loop {
let per_output_change =
total_change.div_with_remainder(to_nonzero_u64(usize::from(split_count)));
if *per_output_change.quotient() >= min_split_output_value {
return split_count;
} else if let Some(new_count) = NonZeroUsize::new(usize::from(split_count) - 1) {
split_count = new_count;
} else {
return NonZeroUsize::MIN;
}
}
} else {
NonZeroUsize::MIN
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EphemeralBalance {
Input(Zatoshis),
Output(Zatoshis),
}
impl EphemeralBalance {
pub fn is_input(&self) -> bool {
matches!(self, EphemeralBalance::Input(_))
}
pub fn is_output(&self) -> bool {
matches!(self, EphemeralBalance::Output(_))
}
pub fn ephemeral_input_amount(&self) -> Option<Zatoshis> {
match self {
EphemeralBalance::Input(v) => Some(*v),
EphemeralBalance::Output(_) => None,
}
}
pub fn ephemeral_output_amount(&self) -> Option<Zatoshis> {
match self {
EphemeralBalance::Input(_) => None,
EphemeralBalance::Output(v) => Some(*v),
}
}
}
pub trait MetaSource {
type Error;
type AccountId;
type NoteRef;
}
impl MetaSource for Infallible {
type Error = Infallible;
type AccountId = Infallible;
type NoteRef = Infallible;
}
impl<I: InputSource> MetaSource for I {
type Error = I::Error;
type AccountId = I::AccountId;
type NoteRef = I::NoteRef;
}
pub trait ChangeStrategy {
type FeeRule: FeeRule + Clone;
type Error: From<<Self::FeeRule as FeeRule>::Error>;
type MetaSource: MetaSource;
type AccountMetaT;
fn fee_rule(&self) -> &Self::FeeRule;
fn fetch_wallet_meta(
&self,
meta_source: &Self::MetaSource,
account: <Self::MetaSource as MetaSource>::AccountId,
target_height: TargetHeight,
exclude: &[<Self::MetaSource as MetaSource>::NoteRef],
) -> Result<Self::AccountMetaT, <Self::MetaSource as MetaSource>::Error>;
#[allow(clippy::too_many_arguments)]
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
&self,
params: &P,
target_height: TargetHeight,
anchor_height: BlockHeight,
zip318: &PoolMigrationParams,
transparent_inputs: &[impl transparent::InputView],
transparent_outputs: &[impl transparent::OutputView],
sapling: &impl sapling::BundleView<NoteRefT>,
#[cfg(feature = "orchard")] orchard: &impl orchard::BundleView<NoteRefT>,
#[cfg(feature = "orchard")] ironwood: &impl orchard::BundleView<NoteRefT>,
ephemeral_balance: Option<EphemeralBalance>,
wallet_meta: &Self::AccountMetaT,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>;
}
#[cfg(test)]
pub(crate) mod tests {
#[cfg(feature = "orchard")]
use {
zcash_primitives::transaction::fees::zip317::MARGINAL_FEE,
zcash_protocol::consensus::{BlockHeight, MAIN_NETWORK},
};
use ::transparent::bundle::{OutPoint, TxOut};
use zcash_primitives::transaction::fees::transparent;
use zcash_protocol::value::Zatoshis;
#[test]
#[cfg(feature = "orchard")]
fn canonical_crossing_fee_is_three_marginal_fees() {
let fee = super::canonical_crossing_fee(&MAIN_NETWORK, BlockHeight::from_u32(2_000_000))
.expect("the canonical shape is a valid input to the ZIP 317 rule");
assert_eq!(fee, (MARGINAL_FEE * 3u64).expect("a valid amount"));
assert_eq!(u64::from(fee), 15_000);
}
use super::sapling;
#[derive(Debug)]
pub(crate) struct TestTransparentInput {
pub outpoint: OutPoint,
pub coin: TxOut,
}
impl transparent::InputView for TestTransparentInput {
fn outpoint(&self) -> &OutPoint {
&self.outpoint
}
fn coin(&self) -> &TxOut {
&self.coin
}
}
pub(crate) struct TestSaplingInput {
pub note_id: u32,
pub value: Zatoshis,
}
impl sapling::InputView<u32> for TestSaplingInput {
fn note_id(&self) -> &u32 {
&self.note_id
}
fn value(&self) -> Zatoshis {
self.value
}
}
#[cfg(feature = "orchard")]
pub(crate) struct TestOrchardInput {
pub note_id: u32,
pub value: Zatoshis,
}
#[cfg(feature = "orchard")]
impl super::orchard::InputView<u32> for TestOrchardInput {
fn note_id(&self) -> &u32 {
&self.note_id
}
fn value(&self) -> Zatoshis {
self.value
}
}
}