Skip to content

Commit

Permalink
Merge pull request #1580 from nuttycom/wallet/multi_output_dynamic_mi…
Browse files Browse the repository at this point in the history
…n_split

zcash_client_backend: Generalize & extend wallet metadata query API
  • Loading branch information
nuttycom authored Nov 13, 2024
2 parents 8b49ca8 + 00cafa3 commit be4bc23
Show file tree
Hide file tree
Showing 19 changed files with 728 additions and 196 deletions.
2 changes: 2 additions & 0 deletions components/zcash_protocol/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this library adheres to Rust's notion of
### Added
- `zcash_protocol::value::QuotRem`
- `zcash_protocol::value::Zatoshis::div_with_remainder`
- `impl Mul<u64> for zcash_protocol::value::Zatoshis`
- `impl Div<NonZeroU64> for zcash_protocol::value::Zatoshis`

### Changed
- MSRV is now 1.77.0.
Expand Down
24 changes: 22 additions & 2 deletions components/zcash_protocol/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::convert::{Infallible, TryFrom};
use std::error;
use std::iter::Sum;
use std::num::NonZeroU64;
use std::ops::{Add, Mul, Neg, Sub};
use std::ops::{Add, Div, Mul, Neg, Sub};

use memuse::DynamicUsage;

Expand Down Expand Up @@ -321,6 +321,8 @@ impl Zatoshis {
/// Divides this `Zatoshis` value by the given divisor and returns the quotient and remainder.
pub fn div_with_remainder(&self, divisor: NonZeroU64) -> QuotRem<Zatoshis> {
let divisor = u64::from(divisor);
// `self` is already bounds-checked, and both the quotient and remainder
// are <= self, so we don't need to re-check them in division.
QuotRem {
quotient: Zatoshis(self.0 / divisor),
remainder: Zatoshis(self.0 % divisor),
Expand Down Expand Up @@ -394,11 +396,19 @@ impl Sub<Zatoshis> for Option<Zatoshis> {
}
}

impl Mul<u64> for Zatoshis {
type Output = Option<Self>;

fn mul(self, rhs: u64) -> Option<Zatoshis> {
Zatoshis::from_u64(self.0.checked_mul(rhs)?).ok()
}
}

impl Mul<usize> for Zatoshis {
type Output = Option<Self>;

fn mul(self, rhs: usize) -> Option<Zatoshis> {
Zatoshis::from_u64(self.0.checked_mul(u64::try_from(rhs).ok()?)?).ok()
self * u64::try_from(rhs).ok()?
}
}

Expand All @@ -414,6 +424,16 @@ impl<'a> Sum<&'a Zatoshis> for Option<Zatoshis> {
}
}

impl Div<NonZeroU64> for Zatoshis {
type Output = Zatoshis;

fn div(self, rhs: NonZeroU64) -> Zatoshis {
// `self` is already bounds-checked and the quotient is <= self, so
// we don't need to re-check it
Zatoshis(self.0 / u64::from(rhs))
}
}

/// A type for balance violations in amount addition and subtraction
/// (overflow and underflow of allowed ranges)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
Expand Down
9 changes: 6 additions & 3 deletions zcash_client_backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::data_api`:
- `Progress`
- `WalletSummary::progress`
- `WalletMeta`
- `PoolMeta`
- `AccountMeta`
- `impl Default for wallet::input_selection::GreedyInputSelector`
- `BoundedU8`
- `NoteFilter`
- `zcash_client_backend::fees`
- `SplitPolicy`
- `StandardFeeRule` has been moved here from `zcash_primitives::fees`. Relative
Expand All @@ -32,7 +35,7 @@ and this library adheres to Rust's notion of
- MSRV is now 1.77.0.
- Migrated to `arti-client 0.23`.
- `zcash_client_backend::data_api`:
- `InputSource` has an added method `get_wallet_metadata`
- `InputSource` has an added method `get_account_metadata`
- `error::Error` has additional variant `Error::Change`. This necessitates
the addition of two type parameters to the `Error` type,
`ChangeErrT` and `NoteRefT`.
Expand Down Expand Up @@ -65,7 +68,7 @@ and this library adheres to Rust's notion of
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
and `AccountMetaT`, 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.
- `zip317::SingleOutputChangeStrategy` has been made polymorphic in the fee
Expand Down
237 changes: 198 additions & 39 deletions zcash_client_backend/src/data_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,35 @@ impl<NoteRef> SpendableNotes<NoteRef> {
}
}

/// Metadata about the structure of unspent outputs in a single pool within a wallet account.
///
/// This type is often used to represent a filtered view of outputs in the account that were
/// selected according to the conditions imposed by a [`NoteFilter`].
#[derive(Debug, Clone)]
pub struct PoolMeta {
note_count: usize,
value: NonNegativeAmount,
}

impl PoolMeta {
/// Constructs a new [`PoolMeta`] value from its constituent parts.
pub fn new(note_count: usize, value: NonNegativeAmount) -> Self {
Self { note_count, value }
}

/// Returns the number of unspent outputs in the account, potentially selected in accordance
/// with some [`NoteFilter`].
pub fn note_count(&self) -> usize {
self.note_count
}

/// Returns the total value of unspent outputs in the account that are accounted for in
/// [`Self::note_count`].
pub fn value(&self) -> NonNegativeAmount {
self.value
}
}

/// 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
Expand All @@ -802,58 +831,185 @@ impl<NoteRef> SpendableNotes<NoteRef> {
/// Values of this type are intended to be used in selection of change output values. A value of
/// this type may represent filtered data, and may therefore not count all of the unspent notes in
/// the wallet.
pub struct WalletMeta {
sapling_note_count: usize,
#[cfg(feature = "orchard")]
orchard_note_count: usize,
///
/// A [`AccountMeta`] value is normally produced by querying the wallet database via passing a
/// [`NoteFilter`] to [`InputSource::get_account_metadata`].
#[derive(Debug, Clone)]
pub struct AccountMeta {
sapling: Option<PoolMeta>,
orchard: Option<PoolMeta>,
}

impl WalletMeta {
/// Constructs a new [`WalletMeta`] value from its constituent parts.
pub fn new(
sapling_note_count: usize,
#[cfg(feature = "orchard")] orchard_note_count: usize,
) -> Self {
Self {
sapling_note_count,
#[cfg(feature = "orchard")]
orchard_note_count,
}
impl AccountMeta {
/// Constructs a new [`AccountMeta`] value from its constituent parts.
pub fn new(sapling: Option<PoolMeta>, orchard: Option<PoolMeta>) -> Self {
Self { sapling, orchard }
}

/// Returns metadata about Sapling notes belonging to the account for which this was generated.
///
/// Returns [`None`] if no metadata is available or it was not possible to evaluate the query
/// described by a [`NoteFilter`] given the available wallet data.
pub fn sapling(&self) -> Option<&PoolMeta> {
self.sapling.as_ref()
}

/// Returns metadata about Orchard notes belonging to the account for which this was generated.
///
/// Returns [`None`] if no metadata is available or it was not possible to evaluate the query
/// described by a [`NoteFilter`] given the available wallet data.
pub fn orchard(&self) -> Option<&PoolMeta> {
self.orchard.as_ref()
}

fn sapling_note_count(&self) -> Option<usize> {
self.sapling.as_ref().map(|m| m.note_count)
}

fn orchard_note_count(&self) -> Option<usize> {
self.orchard.as_ref().map(|m| m.note_count)
}

/// Returns the number of unspent notes in the wallet for the given shielded protocol.
pub fn note_count(&self, protocol: ShieldedProtocol) -> usize {
pub fn note_count(&self, protocol: ShieldedProtocol) -> Option<usize> {
match protocol {
ShieldedProtocol::Sapling => self.sapling_note_count,
#[cfg(feature = "orchard")]
ShieldedProtocol::Orchard => self.orchard_note_count,
#[cfg(not(feature = "orchard"))]
ShieldedProtocol::Orchard => 0,
ShieldedProtocol::Sapling => self.sapling_note_count(),
ShieldedProtocol::Orchard => self.orchard_note_count(),
}
}

/// Returns the total number of unspent shielded notes belonging to the account for which this
/// was generated.
///
/// Returns [`None`] if no metadata is available or it was not possible to evaluate the query
/// described by a [`NoteFilter`] given the available wallet data. If metadata is available
/// only for a single pool, the metadata for that pool will be returned.
pub fn total_note_count(&self) -> Option<usize> {
let s = self.sapling_note_count();
let o = self.orchard_note_count();
s.zip(o).map(|(s, o)| s + o).or(s).or(o)
}

fn sapling_value(&self) -> Option<NonNegativeAmount> {
self.sapling.as_ref().map(|m| m.value)
}

fn orchard_value(&self) -> Option<NonNegativeAmount> {
self.orchard.as_ref().map(|m| m.value)
}

/// Returns the total value of shielded notes represented by [`Self::total_note_count`]
///
/// Returns [`None`] if no metadata is available or it was not possible to evaluate the query
/// described by a [`NoteFilter`] given the available wallet data. If metadata is available
/// only for a single pool, the metadata for that pool will be returned.
pub fn total_value(&self) -> Option<NonNegativeAmount> {
let s = self.sapling_value();
let o = self.orchard_value();
s.zip(o)
.map(|(s, o)| (s + o).expect("Does not overflow Zcash maximum value."))
.or(s)
.or(o)
}
}

/// A `u8` value in the range 0..=MAX
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct BoundedU8<const MAX: u8>(u8);

impl<const MAX: u8> BoundedU8<MAX> {
/// Creates a constant `BoundedU8` from a [`u8`] value.
///
/// Panics: if the value is outside the range `0..=MAX`.
pub const fn new_const(value: u8) -> Self {
assert!(value <= MAX);
Self(value)
}

/// Creates a `BoundedU8` from a [`u8`] value.
///
/// Returns `None` if the provided value is outside the range `0..=MAX`.
pub fn new(value: u8) -> Option<Self> {
if value <= MAX {
Some(Self(value))
} else {
None
}
}

/// Returns the number of unspent Sapling notes belonging to the account for which this was
/// generated.
pub fn sapling_note_count(&self) -> usize {
self.sapling_note_count
/// Returns the wrapped [`u8`] value.
pub fn value(&self) -> u8 {
self.0
}
}

/// Returns the number of unspent Orchard notes belonging to the account for which this was
/// generated.
#[cfg(feature = "orchard")]
pub fn orchard_note_count(&self) -> usize {
self.orchard_note_count
impl<const MAX: u8> From<BoundedU8<MAX>> for u8 {
fn from(value: BoundedU8<MAX>) -> Self {
value.0
}
}

/// Returns the total number of unspent shielded notes belonging to the account for which this
/// was generated.
pub fn total_note_count(&self) -> usize {
self.sapling_note_count + self.note_count(ShieldedProtocol::Orchard)
impl<const MAX: u8> From<BoundedU8<MAX>> for usize {
fn from(value: BoundedU8<MAX>) -> Self {
usize::from(value.0)
}
}

/// A small query language for filtering notes belonging to an account.
///
/// A filter described using this language is applied to notes individually. It is primarily
/// intended for retrieval of account metadata in service of making determinations for how to
/// allocate change notes, and is not currently intended for use in broader note selection
/// contexts.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NoteFilter {
/// Selects notes having value greater than or equal to the provided value.
ExceedsMinValue(NonNegativeAmount),
/// Selects notes having value greater than or equal to approximately the n'th percentile of
/// previously sent notes in the account, irrespective of pool. The wrapped value must be in
/// the range `1..=99`. The value `n` is respected in a best-effort fashion; results are likely
/// to be inaccurate if the account has not yet completed scanning or if insufficient send data
/// is available to establish a distribution.
// TODO: it might be worthwhile to add an optional parameter here that can be used to ignore
// low-valued (test/memo-only) sends when constructing the distribution to be drawn from.
ExceedsPriorSendPercentile(BoundedU8<99>),
/// Selects notes having value greater than or equal to the specified percentage of the account
/// balance across all shielded pools. The wrapped value must be in the range `1..=99`
ExceedsBalancePercentage(BoundedU8<99>),
/// A note will be selected if it satisfies both of the specified conditions.
///
/// If it is not possible to evaluate one of the conditions (for example,
/// [`NoteFilter::ExceedsPriorSendPercentile`] cannot be evaluated if no sends have been
/// performed) then that condition will be ignored. If neither condition can be evaluated,
/// then the entire condition cannot be evaluated.
Combine(Box<NoteFilter>, Box<NoteFilter>),
/// A note will be selected if it satisfies the first condition; if it is not possible to
/// evaluate that condition (for example, [`NoteFilter::ExceedsPriorSendPercentile`] cannot
/// be evaluated if no sends have been performed) then the second condition will be used for
/// evaluation.
Attempt {
condition: Box<NoteFilter>,
fallback: Box<NoteFilter>,
},
}

impl NoteFilter {
/// Constructs a [`NoteFilter::Combine`] query node.
pub fn combine(l: NoteFilter, r: NoteFilter) -> Self {
Self::Combine(Box::new(l), Box::new(r))
}

/// Constructs a [`NoteFilter::Attempt`] query node.
pub fn attempt(condition: NoteFilter, fallback: NoteFilter) -> Self {
Self::Attempt {
condition: Box::new(condition),
fallback: Box::new(fallback),
}
}
}

/// A trait representing the capability to query a data store for unspent transaction outputs
/// belonging to a wallet.
/// belonging to a account.
#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
pub trait InputSource {
/// The type of errors produced by a wallet backend.
Expand Down Expand Up @@ -900,14 +1056,17 @@ pub trait InputSource {
///
/// The returned metadata value must exclude:
/// - spent notes;
/// - unspent notes having value less than the specified minimum value;
/// - unspent notes excluded by the provided selector;
/// - unspent notes identified in the given `exclude` list.
fn get_wallet_metadata(
///
/// Implementations of this method may limit the complexity of supported queries. Such
/// limitations should be clearly documented for the implementing type.
fn get_account_metadata(
&self,
account: Self::AccountId,
min_value: NonNegativeAmount,
selector: &NoteFilter,
exclude: &[Self::NoteRef],
) -> Result<WalletMeta, Self::Error>;
) -> Result<AccountMeta, Self::Error>;

/// Fetches the transparent output corresponding to the provided `outpoint`.
///
Expand Down
Loading

0 comments on commit be4bc23

Please sign in to comment.