Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zcash_client_backend: Generalize & extend wallet metadata query API #1580

Merged
merged 2 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
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 {
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
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 @@
}
}

/// 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 {
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
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

Check warning on line 816 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L815-L816

Added lines #L815 - L816 were not covered by tests
}

/// 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

Check warning on line 822 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L821-L822

Added lines #L821 - L822 were not covered by tests
}
}

/// 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 @@
/// 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()

Check warning on line 854 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L853-L854

Added lines #L853 - L854 were not covered by tests
}

/// 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()

Check warning on line 862 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L861-L862

Added lines #L861 - L862 were not covered by tests
}

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.
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
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(),

Check warning on line 877 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L877

Added line #L877 was not covered by tests
}
}

/// Returns the total number of unspent shielded notes belonging to the account for which this
/// was generated.
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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`]
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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))

Check warning on line 934 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L932-L934

Added lines #L932 - L934 were not covered by tests
} else {
None

Check warning on line 936 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L936

Added line #L936 was not covered by tests
}
}

/// 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

Check warning on line 948 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L947-L948

Added lines #L947 - L948 were not covered by tests
}
}

/// 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.
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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,
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
/// [`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))

Check warning on line 999 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L998-L999

Added lines #L998 - L999 were not covered by tests
}

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

Check warning on line 1003 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L1003

Added line #L1003 was not covered by tests
Self::Attempt {
condition: Box::new(condition),
fallback: Box::new(fallback),

Check warning on line 1006 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L1005-L1006

Added lines #L1005 - L1006 were not covered by tests
}
}
}

/// 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 @@
///
/// 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
Loading