Skip to content

Commit

Permalink
zcash_client_backend: Generalize & extend wallet metadata query API
Browse files Browse the repository at this point in the history
This generalizes the previous wallet metadata query API to be able to
represent more complex queries, and also to return note totals in
addition to note counts.
  • Loading branch information
nuttycom committed Nov 11, 2024
1 parent 8b49ca8 commit e9ad4a6
Show file tree
Hide file tree
Showing 13 changed files with 467 additions and 108 deletions.
22 changes: 20 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,7 @@ 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, so we don't need to re-check it in division
QuotRem {
quotient: Zatoshis(self.0 / divisor),
remainder: Zatoshis(self.0 % divisor),
Expand Down Expand Up @@ -394,11 +395,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 +423,15 @@ impl<'a> Sum<&'a Zatoshis> for Option<Zatoshis> {
}
}

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

fn div(self, rhs: NonZeroU64) -> Zatoshis {

Check warning on line 429 in components/zcash_protocol/src/value.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_protocol/src/value.rs#L429

Added line #L429 was not covered by tests
// `self` is already bounds-checked, so we don't need to re-check it
Zatoshis(self.0 / u64::from(rhs))

Check warning on line 431 in components/zcash_protocol/src/value.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_protocol/src/value.rs#L431

Added line #L431 was not covered by tests
}
}

/// A type for balance violations in amount addition and subtraction
/// (overflow and underflow of allowed ranges)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
Expand Down
2 changes: 2 additions & 0 deletions zcash_client_backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this library adheres to Rust's notion of
- `WalletSummary::progress`
- `WalletMeta`
- `impl Default for wallet::input_selection::GreedyInputSelector`
- `BoundedU8`
- `NoteSelector`
- `zcash_client_backend::fees`
- `SplitPolicy`
- `StandardFeeRule` has been moved here from `zcash_primitives::fees`. Relative
Expand Down
121 changes: 119 additions & 2 deletions zcash_client_backend/src/data_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,20 +804,28 @@ impl<NoteRef> SpendableNotes<NoteRef> {
/// the wallet.
pub struct WalletMeta {
sapling_note_count: usize,
sapling_total_value: NonNegativeAmount,
#[cfg(feature = "orchard")]
orchard_note_count: usize,
#[cfg(feature = "orchard")]
orchard_total_value: NonNegativeAmount,
}

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

Expand All @@ -838,18 +846,124 @@ impl WalletMeta {
self.sapling_note_count
}

/// Returns the total value of Sapling notes represented by [`Self::sapling_note_count`].
pub fn sapling_total_value(&self) -> NonNegativeAmount {
self.sapling_total_value

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L850-L851

Added lines #L850 - L851 were not covered by tests
}

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

/// Returns the total value of Orchard notes represented by [`Self::orchard_note_count`].
#[cfg(feature = "orchard")]
pub fn orchard_total_value(&self) -> NonNegativeAmount {
self.orchard_total_value
}

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

/// Returns the total value of shielded notes represented by [`Self::total_note_count`]
pub fn total_value(&self) -> NonNegativeAmount {
#[cfg(feature = "orchard")]
let orchard_value = self.orchard_total_value;
#[cfg(not(feature = "orchard"))]
let orchard_value = NonNegativeAmount::ZERO;

(self.sapling_total_value + orchard_value).expect("Does not overflow Zcash maximum value.")
}
}

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

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L892-L894

Added lines #L892 - L894 were not covered by tests
}

/// 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 902 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L900-L902

Added lines #L900 - L902 were not covered by tests
} else {
None

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L904

Added line #L904 was not covered by tests
}
}

/// Returns the wrapped [`u8`] value.
pub fn value(&self) -> u8 {
self.0

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L909-L910

Added lines #L909 - L910 were not covered by tests
}
}

impl<const MAX: u8> From<BoundedU8<MAX>> for u8 {
fn from(value: BoundedU8<MAX>) -> Self {
value.0

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L915-L916

Added lines #L915 - L916 were not covered by tests
}
}

impl<const MAX: u8> From<BoundedU8<MAX>> for usize {
fn from(value: BoundedU8<MAX>) -> Self {
usize::from(value.0)

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L921-L922

Added lines #L921 - L922 were not covered by tests
}
}

/// A small query language for filtering notes belonging to an account.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NoteSelector {
/// Selects notes having value greater than or equal to the provided value.
ExceedsMinValue(NonNegativeAmount),
/// Selects notes having value greater than or equal to the n'th percentile of previously sent
/// notes in the wallet. The wrapped value must be in the range `1..=99`. `n` may be rounded
/// to a multiple of 10 as part of this computation.
ExceedsPriorSendPercentile(BoundedU8<99>),
/// Selects notes having value greater than or equal to the specified percentage of the wallet
/// balance. 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,
/// [`NoteSelector::ExceedsPriorSendPercentile`] cannot be evaluated if no sends have been
/// performed) then that condition will be ignored.
And(Box<NoteSelector>, Box<NoteSelector>),
/// A note will be selected if it satisfies the first condition; if it is not possible to
/// evaluate that condition (for example, [`NoteSelector::ExceedsPriorSendPercentile`] cannot
/// be evaluated if no sends have been performed) then the second condition will be used for
/// evaluation.
Attempt {
condition: Box<NoteSelector>,
fallback: Box<NoteSelector>,
},
}

impl NoteSelector {
/// Constructs a [`NoteSelector::And`] query node.
pub fn and(l: NoteSelector, r: NoteSelector) -> Self {
Self::And(Box::new(l), Box::new(r))

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L956-L957

Added lines #L956 - L957 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L961

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

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L963-L964

Added lines #L963 - L964 were not covered by tests
}
}
}

/// A trait representing the capability to query a data store for unspent transaction outputs
Expand Down Expand Up @@ -900,12 +1014,15 @@ 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.
///
/// Implementations of this method may limit the complexity of supported queries. Such
/// limitations should be clearly documented for the implementing type.
fn get_wallet_metadata(
&self,
account: Self::AccountId,
min_value: NonNegativeAmount,
selector: &NoteSelector,
exclude: &[Self::NoteRef],
) -> Result<WalletMeta, Self::Error>;

Expand Down
4 changes: 2 additions & 2 deletions zcash_client_backend/src/data_api/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ use crate::{
ShieldedProtocol,
};

use super::error::Error;
use super::{
chain::{scan_cached_blocks, BlockSource, ChainState, CommitmentTreeRoot, ScanSummary},
scanning::ScanRange,
Expand All @@ -74,6 +73,7 @@ use super::{
WalletCommitmentTrees, WalletMeta, WalletRead, WalletSummary, WalletTest, WalletWrite,
SAPLING_SHARD_HEIGHT,
};
use super::{error::Error, NoteSelector};

#[cfg(feature = "transparent-inputs")]
use {
Expand Down Expand Up @@ -2354,7 +2354,7 @@ impl InputSource for MockWalletDb {
fn get_wallet_metadata(
&self,
_account: Self::AccountId,
_min_value: NonNegativeAmount,
_selector: &NoteSelector,
_exclude: &[Self::NoteRef],
) -> Result<WalletMeta, Self::Error> {
Err(())
Expand Down
28 changes: 14 additions & 14 deletions zcash_client_backend/src/data_api/testing/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ pub fn send_with_multiple_change_outputs<T: ShieldedPoolTester>(
Some(change_memo.clone().into()),
T::SHIELDED_PROTOCOL,
DustOutputPolicy::default(),
SplitPolicy::new(
SplitPolicy::with_min_output_value(
NonZeroUsize::new(2).unwrap(),
NonNegativeAmount::const_from_u64(100_0000),
),
Expand Down Expand Up @@ -465,7 +465,7 @@ pub fn send_with_multiple_change_outputs<T: ShieldedPoolTester>(
Some(change_memo.into()),
T::SHIELDED_PROTOCOL,
DustOutputPolicy::default(),
SplitPolicy::new(
SplitPolicy::with_min_output_value(
NonZeroUsize::new(8).unwrap(),
NonNegativeAmount::const_from_u64(10_0000),
),
Expand Down Expand Up @@ -530,7 +530,7 @@ pub fn send_multi_step_proposed_transfer<T: ShieldedPoolTester, DSF>(
// Add funds to the wallet.
add_funds(st, value);

let expected_step0_fee = (zip317::MARGINAL_FEE * 3).unwrap();
let expected_step0_fee = (zip317::MARGINAL_FEE * 3u64).unwrap();
let expected_step1_fee = zip317::MINIMUM_FEE;
let expected_ephemeral = (transfer_amount + expected_step1_fee).unwrap();
let expected_step0_change =
Expand Down Expand Up @@ -1123,7 +1123,7 @@ pub fn spend_fails_on_unverified_notes<T: ShieldedPoolTester>(
st.scan_cached_blocks(h2 + 1, 8);

// Total balance is value * number of blocks scanned (10).
assert_eq!(st.get_total_balance(account_id), (value * 10).unwrap());
assert_eq!(st.get_total_balance(account_id), (value * 10u64).unwrap());

// Spend still fails
assert_matches!(
Expand All @@ -1150,15 +1150,15 @@ pub fn spend_fails_on_unverified_notes<T: ShieldedPoolTester>(
st.scan_cached_blocks(h11, 1);

// Total balance is value * number of blocks scanned (11).
assert_eq!(st.get_total_balance(account_id), (value * 11).unwrap());
assert_eq!(st.get_total_balance(account_id), (value * 11u64).unwrap());
// Spendable balance at 10 confirmations is value * 2.
assert_eq!(
st.get_spendable_balance(account_id, 10),
(value * 2).unwrap()
(value * 2u64).unwrap()
);
assert_eq!(
st.get_pending_shielded_balance(account_id, 10),
(value * 9).unwrap()
(value * 9u64).unwrap()
);

// Should now be able to generate a proposal
Expand Down Expand Up @@ -1192,7 +1192,7 @@ pub fn spend_fails_on_unverified_notes<T: ShieldedPoolTester>(
// TODO: send to an account so that we can check its balance.
assert_eq!(
st.get_total_balance(account_id),
((value * 11).unwrap()
((value * 11u64).unwrap()
- (amount_sent + NonNegativeAmount::from_u64(10000).unwrap()).unwrap())
.unwrap()
);
Expand Down Expand Up @@ -2124,7 +2124,7 @@ pub fn fully_funded_fully_private<P0: ShieldedPoolTester, P1: ShieldedPoolTester
st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value);
st.scan_cached_blocks(account.birthday().height(), 2);

let initial_balance = (note_value * 2).unwrap();
let initial_balance = (note_value * 2u64).unwrap();

Check warning on line 2127 in zcash_client_backend/src/data_api/testing/pool.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/testing/pool.rs#L2127

Added line #L2127 was not covered by tests
assert_eq!(st.get_total_balance(account.id()), initial_balance);
assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance);

Expand Down Expand Up @@ -2214,7 +2214,7 @@ pub fn fully_funded_send_to_t<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value);
st.scan_cached_blocks(account.birthday().height(), 2);

let initial_balance = (note_value * 2).unwrap();
let initial_balance = (note_value * 2u64).unwrap();

Check warning on line 2217 in zcash_client_backend/src/data_api/testing/pool.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/testing/pool.rs#L2217

Added line #L2217 was not covered by tests
assert_eq!(st.get_total_balance(account.id()), initial_balance);
assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance);

Expand Down Expand Up @@ -2307,7 +2307,7 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(

let next_to_scan = scanned.scanned_range().end;

let initial_balance = (note_value * 3).unwrap();
let initial_balance = (note_value * 3u64).unwrap();

Check warning on line 2310 in zcash_client_backend/src/data_api/testing/pool.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/testing/pool.rs#L2310

Added line #L2310 was not covered by tests
assert_eq!(st.get_total_balance(acct_id), initial_balance);
assert_eq!(st.get_spendable_balance(acct_id, 1), initial_balance);

Expand Down Expand Up @@ -2352,7 +2352,7 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
let expected_change = (note_value - transfer_amount - expected_fee).unwrap();
assert_eq!(
st.get_total_balance(acct_id),
((note_value * 2).unwrap() + expected_change).unwrap()
((note_value * 2u64).unwrap() + expected_change).unwrap()

Check warning on line 2355 in zcash_client_backend/src/data_api/testing/pool.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/testing/pool.rs#L2355

Added line #L2355 was not covered by tests
);
assert_eq!(st.get_pending_change(acct_id, 1), expected_change);

Expand Down Expand Up @@ -2396,8 +2396,8 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
);

let expected_final = (initial_balance + note_value
- (transfer_amount * 3).unwrap()
- (expected_fee * 3).unwrap())
- (transfer_amount * 3u64).unwrap()
- (expected_fee * 3u64).unwrap())

Check warning on line 2400 in zcash_client_backend/src/data_api/testing/pool.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/testing/pool.rs#L2399-L2400

Added lines #L2399 - L2400 were not covered by tests
.unwrap();
assert_eq!(st.get_total_balance(acct_id), expected_final);

Expand Down
Loading

0 comments on commit e9ad4a6

Please sign in to comment.