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 12, 2024
1 parent 8b49ca8 commit 530d06b
Show file tree
Hide file tree
Showing 16 changed files with 655 additions and 159 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 {
// `self` is already bounds-checked, 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
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
189 changes: 156 additions & 33 deletions zcash_client_backend/src/data_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,20 @@ impl<NoteRef> SpendableNotes<NoteRef> {
}
}

/// Metadata about the structure of unspent outputs in a single pool within a wallet account.
#[derive(Debug, Clone, Copy)]
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 }
}
}

/// 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,53 +816,159 @@ 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.
#[derive(Debug, Clone, Copy)]
pub struct WalletMeta {
sapling_note_count: usize,
#[cfg(feature = "orchard")]
orchard_note_count: usize,
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,
}
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 [`NoteSelector`] given the available wallet data.
pub fn sapling(&self) -> Option<PoolMeta> {
self.sapling

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L835-L836

Added lines #L835 - L836 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 [`NoteSelector`] given the available wallet data.
pub fn orchard(&self) -> Option<PoolMeta> {
self.orchard

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L843-L844

Added lines #L843 - L844 were not covered by tests
}

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

fn orchard_note_count(&self) -> Option<usize> {
self.orchard.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(),

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L859

Added line #L859 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 total number of unspent shielded notes belonging to the account for which this
/// was generated.
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)
}

/// 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
fn sapling_value(&self) -> Option<NonNegativeAmount> {
self.sapling.map(|m| m.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)
fn orchard_value(&self) -> Option<NonNegativeAmount> {
self.orchard.map(|m| m.value)
}

/// Returns the total value of shielded notes represented by [`Self::total_note_count`]
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 908 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L906-L908

Added lines #L906 - L908 were not covered by tests
} else {
None

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#L910

Added line #L910 was not covered by tests
}
}

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

impl<const MAX: u8> From<BoundedU8<MAX>> for u8 {
fn from(value: BoundedU8<MAX>) -> Self {
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
}
}

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.
#[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 963 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L962-L963

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

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

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L967

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

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

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L969-L970

Added lines #L969 - L970 were not covered by tests
}
}
}

Expand Down Expand Up @@ -900,12 +1020,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
48 changes: 46 additions & 2 deletions zcash_client_backend/src/data_api/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use secrecy::{ExposeSecret, Secret, SecretVec};
use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree};
use subtle::ConditionallySelectable;

use zcash_address::ZcashAddress;
use zcash_keys::address::Address;
use zcash_note_encryption::Domain;
use zcash_primitives::{
Expand All @@ -43,6 +44,7 @@ use zcash_protocol::{
value::{ZatBalance, Zatoshis},
};
use zip32::{fingerprint::SeedFingerprint, DiversifierIndex};
use zip321::Payment;

use crate::{
address::UnifiedAddress,
Expand All @@ -59,7 +61,6 @@ use crate::{
ShieldedProtocol,
};

use super::error::Error;
use super::{
chain::{scan_cached_blocks, BlockSource, ChainState, CommitmentTreeRoot, ScanSummary},
scanning::ScanRange,
Expand All @@ -74,6 +75,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 @@ -861,6 +863,48 @@ where
+ WalletCommitmentTrees,
<DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + 'static,
{
// Creates a transaction that sends the specified value from the given account to
// the provided recipient address, using a greedy input selector and the default
// mutli-output change strategy.
pub fn create_standard_transaction(
&mut self,
from_account: &TestAccount<DbT::Account>,
to: ZcashAddress,
value: NonNegativeAmount,
) -> Result<
NonEmpty<TxId>,
super::wallet::TransferErrT<
DbT,
GreedyInputSelector<DbT>,
standard::MultiOutputChangeStrategy<DbT>,
>,
> {
let input_selector = GreedyInputSelector::new();

let fallback_change_pool = ShieldedProtocol::Sapling;
#[cfg(feature = "orchard")]
let fallback_change_pool = ShieldedProtocol::Orchard;

let change_strategy = standard::SingleOutputChangeStrategy::new(
StandardFeeRule::Zip317,
None,
fallback_change_pool,
DustOutputPolicy::default(),
);

let request =
zip321::TransactionRequest::new(vec![Payment::without_memo(to, value)]).unwrap();

self.spend(
&input_selector,
&change_strategy,
from_account.usk(),
request,
OvkPolicy::Sender,
NonZeroU32::MIN,
)
}

/// Prepares and executes the given [`zip321::TransactionRequest`] in a single step.
#[allow(clippy::type_complexity)]
pub fn spend<InputsT, ChangeT>(
Expand Down Expand Up @@ -2354,7 +2398,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
Loading

0 comments on commit 530d06b

Please sign in to comment.