Skip to content

Commit

Permalink
WIP: zcash_client_backend: Allow change strategies to act based on wa…
Browse files Browse the repository at this point in the history
…llet balance.
  • Loading branch information
nuttycom committed Oct 18, 2024
1 parent 6c95fd8 commit 98093e1
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 68 deletions.
59 changes: 56 additions & 3 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,13 +846,24 @@ 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
}

/// 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 {
Expand All @@ -855,6 +874,37 @@ impl WalletMeta {

self.sapling_note_count + orchard_note_count
}

/// 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 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.
MinValue(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..=100`
PriorSendPercentile(u8),
/// 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..=100`
BalancePercentage(u8),
/// A note will be selected if it satisfies the first condition; if it is not possible to
/// evaaluate that condition (for example, [`NoteSelector::PriorSendPercentile`] cannot be
/// evaluated if no sends have been performed) then the second condition will be used for
/// evaluation.
Try {
condition: Box<NoteSelector>,
fallback: Box<NoteSelector>,
},
}

/// A trait representing the capability to query a data store for unspent transaction outputs
Expand Down Expand Up @@ -903,12 +953,15 @@ pub trait InputSource {

/// Returns metadata describing the structure of the wallet for the specified account.
///
/// The returned metadata value must exclude spent notes and unspent notes having value less
/// than the specified minimum value or identified in the given exclude list.
/// The returned metadata value must include only unspent notes and unspent notes that satisfy
/// the provided selector and are not 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
51 changes: 37 additions & 14 deletions zcash_client_backend/src/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use zcash_primitives::{
};
use zcash_protocol::{PoolType, ShieldedProtocol};

use crate::data_api::InputSource;
use crate::data_api::{InputSource, Ratio};

pub(crate) mod common;
pub mod fixed;
Expand Down Expand Up @@ -344,14 +344,14 @@ impl Default for DustOutputPolicy {
#[derive(Clone, Copy, Debug)]
pub struct SplitPolicy {
target_output_count: NonZeroUsize,
min_split_output_size: NonNegativeAmount,
min_split_output_size: Option<NonNegativeAmount>,
}

impl SplitPolicy {
/// Constructs a new [`SplitPolicy`] from its constituent parts.
pub fn new(
target_output_count: NonZeroUsize,
min_split_output_size: NonNegativeAmount,
min_split_output_size: Option<NonNegativeAmount>,
) -> Self {
Self {
target_output_count,
Expand All @@ -363,15 +363,15 @@ impl SplitPolicy {
pub fn single_output() -> Self {
Self {
target_output_count: NonZeroUsize::MIN,
min_split_output_size: NonNegativeAmount::ZERO,
min_split_output_size: None,
}
}

/// Returns the minimum value for a note resulting from splitting of change.
///
/// If splitting change would result in notes of value less than the minimum split output size,
/// a smaller number of splits should be chosen.
pub fn min_split_output_size(&self) -> NonNegativeAmount {
pub fn min_split_output_size(&self) -> Option<NonNegativeAmount> {
self.min_split_output_size
}

Expand All @@ -380,22 +380,45 @@ impl SplitPolicy {
pub fn split_count(
&self,
existing_notes: usize,
existing_notes_total: NonNegativeAmount,
total_change: NonNegativeAmount,
) -> NonZeroUsize {
let mut split_count =
NonZeroUsize::new(usize::from(self.target_output_count).saturating_sub(existing_notes))
.unwrap_or(NonZeroUsize::MIN);

loop {
let per_output_change = total_change.div_with_remainder(split_count);
if split_count > NonZeroUsize::MIN
&& *per_output_change.quotient() < self.min_split_output_size
{
// safety: `split_count` has just been verified to be > 1
split_count = unsafe { NonZeroUsize::new_unchecked(usize::from(split_count) - 1) };
} else {
return split_count;
let min_split_output_size = self.min_split_output_size.or_else(|| {
// If no minimum split output size is set, we choose the minimum split size to be a
// quarter of the average value of notes in the wallet after the transaction.
(existing_notes_total + total_change).map(|total| {
*total
.div_with_remainder(
self.target_output_count
.saturating_mul(NonZeroUsize::new(4).unwrap()),
)
.quotient()
})
});

if let Some(min_split_output_size) = min_split_output_size {
loop {
let per_output_change = total_change.div_with_remainder(split_count);
if split_count > NonZeroUsize::MIN
&& *per_output_change.quotient() < min_split_output_size
{
// safety: `split_count` has just been verified to be > 1
split_count =
unsafe { NonZeroUsize::new_unchecked(usize::from(split_count) - 1) };
} else {
return split_count;
}
}
} else {
// This is purely defensive; this case would only arise in the case that the addition
// of the existing notes with the total change overflows the maximum monetary amount.
// Since it's always safe to fall back to a single change value, this is better than a
// panic.
return NonZeroUsize::MIN;
}
}
}
Expand Down
12 changes: 9 additions & 3 deletions zcash_client_backend/src/fees/zip317.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ mod tests {

{
// spend a single Sapling note and produce 5 outputs
let balance = |existing_notes| {
let balance = |existing_notes, total| {
change_strategy.compute_balance(
&Network::TestNetwork,
Network::TestNetwork
Expand All @@ -326,14 +326,17 @@ mod tests {
None,
Some(&WalletMeta::new(
existing_notes,
total,
#[cfg(feature = "orchard")]
0,
#[cfg(feature = "orchard")]
NonNegativeAmount::ZERO,
)),
)
};

assert_matches!(
balance(0),
balance(0, NonNegativeAmount::ZERO),
Ok(balance) if
balance.proposed_change() == [
ChangeValue::sapling(NonNegativeAmount::const_from_u64(129_4000), None),
Expand All @@ -346,7 +349,7 @@ mod tests {
);

assert_matches!(
balance(2),
balance(2, NonNegativeAmount::const_from_u64(100_0000)),
Ok(balance) if
balance.proposed_change() == [
ChangeValue::sapling(NonNegativeAmount::const_from_u64(216_0000), None),
Expand Down Expand Up @@ -382,8 +385,11 @@ mod tests {
None,
Some(&WalletMeta::new(
0,
NonNegativeAmount::ZERO,
#[cfg(feature = "orchard")]
0,
#[cfg(feature = "orchard")]
NonNegativeAmount::ZERO,
)),
);

Expand Down
4 changes: 4 additions & 0 deletions zcash_client_sqlite/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ pub enum SqliteClientError {
/// An error occurred in computing wallet balance
BalanceError(BalanceError),

/// A note selection query contained an invalid constant or was otherwise not supported.
NoteSelectorInvalid(NoteSelector),

/// The proposal cannot be constructed until transactions with previously reserved
/// ephemeral address outputs have been mined. The parameters are the account id and
/// the index that could not safely be reserved.
Expand Down Expand Up @@ -187,6 +190,7 @@ impl fmt::Display for SqliteClientError {
SqliteClientError::ChainHeightUnknown => write!(f, "Chain height unknown; please call `update_chain_tip`"),
SqliteClientError::UnsupportedPoolType(t) => write!(f, "Pool type is not currently supported: {}", t),
SqliteClientError::BalanceError(e) => write!(f, "Balance error: {}", e),
SqliteClientError::NoteSelectorInvalid(s) => write!(f, "Could not evaluate selection query: {:?}", s),
#[cfg(feature = "transparent-inputs")]
SqliteClientError::ReachedGapLimit(account_id, bad_index) => write!(f,
"The proposal cannot be constructed until transactions with previously reserved ephemeral address outputs have been mined. \
Expand Down
31 changes: 18 additions & 13 deletions zcash_client_sqlite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ use zcash_client_backend::{
chain::{BlockSource, ChainState, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange},
Account, AccountBirthday, AccountPurpose, AccountSource, BlockMetadata,
DecryptedTransaction, InputSource, NullifierQuery, ScannedBlock, SeedRelevance,
SentTransaction, SpendableNotes, TransactionDataRequest, WalletCommitmentTrees, WalletMeta,
WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
DecryptedTransaction, InputSource, NoteSelector, NullifierQuery, ScannedBlock,
SeedRelevance, SentTransaction, SpendableNotes, TransactionDataRequest,
WalletCommitmentTrees, WalletMeta, WalletRead, WalletSummary, WalletWrite,
SAPLING_SHARD_HEIGHT,
},
keys::{
AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey,
Expand Down Expand Up @@ -128,7 +129,7 @@ pub mod error;
pub mod wallet;
use wallet::{
commitment_tree::{self, put_shard_roots},
common::count_outputs,
common::spendable_notes_meta,
SubtreeProgressEstimator,
};

Expand Down Expand Up @@ -351,30 +352,34 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
fn get_wallet_metadata(
&self,
account_id: Self::AccountId,
min_value: NonNegativeAmount,
selector: &NoteSelector,
exclude: &[Self::NoteRef],
exclude: &[Self::NoteRef],
) -> Result<WalletMeta, Self::Error> {
let sapling_note_count = count_outputs(
let sapling_pool_meta = spendable_notes_meta(
self.conn.borrow(),
ShieldedProtocol::Sapling,
account_id,
min_value,
selector,
exclude,
ShieldedProtocol::Sapling,
)?;

#[cfg(feature = "orchard")]
let orchard_note_count = count_outputs(
let orchard_pool_meta = spendable_notes_meta(
self.conn.borrow(),
ShieldedProtocol::Orchard,
account_id,
min_value,
selector,
exclude,
ShieldedProtocol::Orchard,
)?;

Ok(WalletMeta::new(
sapling_note_count,
sapling_pool_meta.note_count,
sapling_pool_meta.total_value,
#[cfg(feature = "orchard")]
orchard_pool_meta.note_count,
#[cfg(feature = "orchard")]
orchard_note_count,
orchard_pool_meta.total_value,
))
}
}
Expand Down
Loading

0 comments on commit 98093e1

Please sign in to comment.