Add `get_wallet_summary` to `WalletRead`
The intent of this API is to provide a single API which returns in a single call: * per-account balances, including pending values * wallet sync progress Fixes #865 Fixes #900
This commit is contained in:
parent
4e823d92eb
commit
f53ea2d778
|
@ -22,15 +22,19 @@ and this library adheres to Rust's notion of
|
|||
- `impl Eq for zcash_client_backend::zip321::{Payment, TransactionRequest}`
|
||||
- `impl Debug` for `zcash_client_backend::{data_api::wallet::input_selection::Proposal, wallet::ReceivedSaplingNote}`
|
||||
- `zcash_client_backend::data_api`:
|
||||
- `AccountBalance`
|
||||
- `AccountBirthday`
|
||||
- `Balance`
|
||||
- `BlockMetadata`
|
||||
- `NoteId`
|
||||
- `NullifierQuery` for use with `WalletRead::get_sapling_nullifiers`
|
||||
- `Ratio`
|
||||
- `ScannedBlock`
|
||||
- `ShieldedProtocol`
|
||||
- `WalletCommitmentTrees`
|
||||
- `WalletSummary`
|
||||
- `WalletRead::{chain_height, block_metadata, block_fully_scanned, suggest_scan_ranges,
|
||||
get_wallet_birthday, get_account_birthday}`
|
||||
get_wallet_birthday, get_account_birthday, get_wallet_summary}`
|
||||
- `WalletWrite::{put_blocks, update_chain_tip}`
|
||||
- `chain::CommitmentTreeRoot`
|
||||
- `scanning` A new module containing types required for `suggest_scan_ranges`
|
||||
|
@ -113,6 +117,8 @@ and this library adheres to Rust's notion of
|
|||
instead to obtain the wallet's view of the chain tip instead, or
|
||||
`suggest_scan_ranges` to obtain information about blocks that need to be
|
||||
scanned.
|
||||
- `WalletRead::get_balance_at` has been removed. Use `WalletRead::get_wallet_summary`
|
||||
instead.
|
||||
- `WalletRead::{get_all_nullifiers, get_commitment_tree, get_witnesses}` have
|
||||
been removed without replacement. The utility of these methods is now
|
||||
subsumed by those available from the `WalletCommitmentTrees` trait.
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
//! Interfaces for wallet data persistence & low-level wallet utilities.
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::io;
|
||||
use std::num::NonZeroU32;
|
||||
use std::{collections::HashMap, num::TryFromIntError};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::Debug,
|
||||
io,
|
||||
num::{NonZeroU32, TryFromIntError},
|
||||
};
|
||||
|
||||
use incrementalmerkletree::{frontier::Frontier, Retention};
|
||||
use secrecy::SecretVec;
|
||||
|
@ -15,7 +17,10 @@ use zcash_primitives::{
|
|||
memo::{Memo, MemoBytes},
|
||||
sapling::{self, Node, NOTE_COMMITMENT_TREE_DEPTH},
|
||||
transaction::{
|
||||
components::{amount::Amount, OutPoint},
|
||||
components::{
|
||||
amount::{Amount, NonNegativeAmount},
|
||||
OutPoint,
|
||||
},
|
||||
Transaction, TxId,
|
||||
},
|
||||
zip32::{AccountId, ExtendedFullViewingKey},
|
||||
|
@ -46,6 +51,155 @@ pub enum NullifierQuery {
|
|||
All,
|
||||
}
|
||||
|
||||
/// Balance information for a value within a single shielded pool in an account.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Balance {
|
||||
/// The value in the account that may currently be spent; it is possible to compute witnesses
|
||||
/// for all the notes that comprise this value, and all of this value is confirmed to the
|
||||
/// required confirmation depth.
|
||||
pub spendable_value: NonNegativeAmount,
|
||||
|
||||
/// The value in the account of shielded change notes that do not yet have sufficient
|
||||
/// confirmations to be spendable.
|
||||
pub change_pending_confirmation: NonNegativeAmount,
|
||||
|
||||
/// The value in the account of all remaining received notes that either do not have sufficient
|
||||
/// confirmations to be spendable, or for which witnesses cannot yet be constructed without
|
||||
/// additional scanning.
|
||||
pub value_pending_spendability: NonNegativeAmount,
|
||||
}
|
||||
|
||||
impl Balance {
|
||||
/// The [`Balance`] value having zero values for all its fields.
|
||||
pub const ZERO: Self = Self {
|
||||
spendable_value: NonNegativeAmount::ZERO,
|
||||
change_pending_confirmation: NonNegativeAmount::ZERO,
|
||||
value_pending_spendability: NonNegativeAmount::ZERO,
|
||||
};
|
||||
|
||||
/// Returns the total value of funds represented by this [`Balance`].
|
||||
pub fn total(&self) -> NonNegativeAmount {
|
||||
(self.spendable_value + self.change_pending_confirmation + self.value_pending_spendability)
|
||||
.expect("Balance cannot overflow MAX_MONEY")
|
||||
}
|
||||
}
|
||||
|
||||
/// Balance information for a single account. The sum of this struct's fields is the total balance
|
||||
/// of the wallet.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct AccountBalance {
|
||||
/// The value of unspent Sapling outputs belonging to the account.
|
||||
pub sapling_balance: Balance,
|
||||
|
||||
/// The value of all unspent transparent outputs belonging to the account, irrespective of
|
||||
/// confirmation depth.
|
||||
///
|
||||
/// Unshielded balances are not subject to confirmation-depth constraints, because the only
|
||||
/// possible operation on a transparent balance is to shield it, it is possible to create a
|
||||
/// zero-conf transaction to perform that shielding, and the resulting shielded notes will be
|
||||
/// subject to normal confirmation rules.
|
||||
pub unshielded: NonNegativeAmount,
|
||||
}
|
||||
|
||||
impl AccountBalance {
|
||||
/// Returns the total value of funds belonging to the account.
|
||||
pub fn total(&self) -> NonNegativeAmount {
|
||||
(self.sapling_balance.total() + self.unshielded)
|
||||
.expect("Account balance cannot overflow MAX_MONEY")
|
||||
}
|
||||
}
|
||||
|
||||
/// A polymorphic ratio type, usually used for rational numbers.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Ratio<T> {
|
||||
numerator: T,
|
||||
denominator: T,
|
||||
}
|
||||
|
||||
impl<T> Ratio<T> {
|
||||
/// Constructs a new Ratio from a numerator and a denominator.
|
||||
pub fn new(numerator: T, denominator: T) -> Self {
|
||||
Self {
|
||||
numerator,
|
||||
denominator,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the numerator of the ratio.
|
||||
pub fn numerator(&self) -> &T {
|
||||
&self.numerator
|
||||
}
|
||||
|
||||
/// Returns the denominator of the ratio.
|
||||
pub fn denominator(&self) -> &T {
|
||||
&self.denominator
|
||||
}
|
||||
}
|
||||
|
||||
/// A type representing the potentially-spendable value of unspent outputs in the wallet.
|
||||
///
|
||||
/// The balances reported using this data structure may overestimate the total spendable value of
|
||||
/// the wallet, in the case that the spend of a previously received shielded note has not yet been
|
||||
/// detected by the process of scanning the chain. The balances reported using this data structure
|
||||
/// can only be certain to be unspent in the case that [`Self::is_synced`] is true, and even in
|
||||
/// this circumstance it is possible that a newly created transaction could conflict with a
|
||||
/// not-yet-mined transaction in the mempool.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WalletSummary {
|
||||
account_balances: BTreeMap<AccountId, AccountBalance>,
|
||||
chain_tip_height: BlockHeight,
|
||||
fully_scanned_height: BlockHeight,
|
||||
sapling_scan_progress: Option<Ratio<u64>>,
|
||||
}
|
||||
|
||||
impl WalletSummary {
|
||||
/// Constructs a new [`WalletSummary`] from its constituent parts.
|
||||
pub fn new(
|
||||
account_balances: BTreeMap<AccountId, AccountBalance>,
|
||||
chain_tip_height: BlockHeight,
|
||||
fully_scanned_height: BlockHeight,
|
||||
sapling_scan_progress: Option<Ratio<u64>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
account_balances,
|
||||
chain_tip_height,
|
||||
fully_scanned_height,
|
||||
sapling_scan_progress,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the balances of accounts in the wallet, keyed by account ID.
|
||||
pub fn account_balances(&self) -> &BTreeMap<AccountId, AccountBalance> {
|
||||
&self.account_balances
|
||||
}
|
||||
|
||||
/// Returns the height of the current chain tip.
|
||||
pub fn chain_tip_height(&self) -> BlockHeight {
|
||||
self.chain_tip_height
|
||||
}
|
||||
|
||||
/// Returns the height below which all blocks wallet have been scanned, ignoring blocks below
|
||||
/// the wallet birthday.
|
||||
pub fn fully_scanned_height(&self) -> BlockHeight {
|
||||
self.fully_scanned_height
|
||||
}
|
||||
|
||||
/// Returns the progress of scanning Sapling outputs, in terms of the ratio between notes
|
||||
/// scanned and the total number of notes added to the chain since the wallet birthday.
|
||||
///
|
||||
/// This ratio should only be used to compute progress percentages, and the numerator and
|
||||
/// denominator should not be treated as authoritative note counts. Returns `None` if the
|
||||
/// wallet is unable to determine the size of the note commitment tree.
|
||||
pub fn sapling_scan_progress(&self) -> Option<Ratio<u64>> {
|
||||
self.sapling_scan_progress
|
||||
}
|
||||
|
||||
/// Returns whether or not wallet scanning is complete.
|
||||
pub fn is_synced(&self) -> bool {
|
||||
self.chain_tip_height == self.fully_scanned_height
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only operations required for light wallet functions.
|
||||
///
|
||||
/// This trait defines the read-only portion of the storage interface atop which
|
||||
|
@ -157,15 +311,12 @@ pub trait WalletRead {
|
|||
extfvk: &ExtendedFullViewingKey,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// Returns the wallet balance for an account as of the specified block height.
|
||||
///
|
||||
/// This may be used to obtain a balance that ignores notes that have been received so recently
|
||||
/// that they are not yet deemed spendable.
|
||||
fn get_balance_at(
|
||||
/// Returns the wallet balances and sync status for an account given the specified minimum
|
||||
/// number of confirmations, or `Ok(None)` if the wallet has no balance data available.
|
||||
fn get_wallet_summary(
|
||||
&self,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Amount, Self::Error>;
|
||||
min_confirmations: u32,
|
||||
) -> Result<Option<WalletSummary>, Self::Error>;
|
||||
|
||||
/// Returns the memo for a note.
|
||||
///
|
||||
|
@ -747,7 +898,7 @@ pub mod testing {
|
|||
use super::{
|
||||
chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata,
|
||||
DecryptedTransaction, NoteId, NullifierQuery, ScannedBlock, SentTransaction,
|
||||
WalletCommitmentTrees, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT,
|
||||
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
|
||||
};
|
||||
|
||||
pub struct MockWalletDb {
|
||||
|
@ -853,12 +1004,11 @@ pub mod testing {
|
|||
Ok(false)
|
||||
}
|
||||
|
||||
fn get_balance_at(
|
||||
fn get_wallet_summary(
|
||||
&self,
|
||||
_account: AccountId,
|
||||
_anchor_height: BlockHeight,
|
||||
) -> Result<Amount, Self::Error> {
|
||||
Ok(Amount::zero())
|
||||
_min_confirmations: u32,
|
||||
) -> Result<Option<WalletSummary>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_memo(&self, _id_note: NoteId) -> Result<Option<Memo>, Self::Error> {
|
||||
|
|
|
@ -66,7 +66,7 @@ use zcash_client_backend::{
|
|||
scanning::{ScanPriority, ScanRange},
|
||||
AccountBirthday, BlockMetadata, DecryptedTransaction, NoteId, NullifierQuery, PoolType,
|
||||
Recipient, ScannedBlock, SentTransaction, ShieldedProtocol, WalletCommitmentTrees,
|
||||
WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT,
|
||||
WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
|
||||
},
|
||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||
proto::compact_formats::CompactBlock,
|
||||
|
@ -88,7 +88,10 @@ pub mod error;
|
|||
pub mod serialization;
|
||||
|
||||
pub mod wallet;
|
||||
use wallet::commitment_tree::{self, put_shard_roots};
|
||||
use wallet::{
|
||||
commitment_tree::{self, put_shard_roots},
|
||||
SubtreeScanProgress,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod testing;
|
||||
|
@ -243,12 +246,11 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
|
|||
wallet::is_valid_account_extfvk(self.conn.borrow(), &self.params, account, extfvk)
|
||||
}
|
||||
|
||||
fn get_balance_at(
|
||||
fn get_wallet_summary(
|
||||
&self,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Amount, Self::Error> {
|
||||
wallet::get_balance_at(self.conn.borrow(), account, anchor_height)
|
||||
min_confirmations: u32,
|
||||
) -> Result<Option<WalletSummary>, Self::Error> {
|
||||
wallet::get_wallet_summary(self.conn.borrow(), min_confirmations, &SubtreeScanProgress)
|
||||
}
|
||||
|
||||
fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error> {
|
||||
|
@ -456,6 +458,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
block.block_hash(),
|
||||
block.block_time(),
|
||||
block.metadata().sapling_tree_size(),
|
||||
block.sapling_commitments().len().try_into().unwrap(),
|
||||
)?;
|
||||
|
||||
for tx in block.transactions() {
|
||||
|
|
|
@ -25,7 +25,7 @@ use zcash_client_backend::{
|
|||
input_selection::{GreedyInputSelectorError, InputSelector, Proposal},
|
||||
propose_transfer, spend,
|
||||
},
|
||||
AccountBirthday, WalletWrite,
|
||||
AccountBirthday, WalletSummary, WalletWrite,
|
||||
},
|
||||
keys::UnifiedSpendingKey,
|
||||
proto::compact_formats::{
|
||||
|
@ -46,7 +46,10 @@ use zcash_primitives::{
|
|||
Note, Nullifier, PaymentAddress,
|
||||
},
|
||||
transaction::{
|
||||
components::{amount::BalanceError, Amount},
|
||||
components::{
|
||||
amount::{BalanceError, NonNegativeAmount},
|
||||
Amount,
|
||||
},
|
||||
fees::FeeRule,
|
||||
TxId,
|
||||
},
|
||||
|
@ -56,7 +59,10 @@ use zcash_primitives::{
|
|||
use crate::{
|
||||
chain::init::init_cache_database,
|
||||
error::SqliteClientError,
|
||||
wallet::{commitment_tree, init::init_wallet_db, sapling::tests::test_prover},
|
||||
wallet::{
|
||||
commitment_tree, get_wallet_summary, init::init_wallet_db, sapling::tests::test_prover,
|
||||
SubtreeScanProgress,
|
||||
},
|
||||
AccountId, ReceivedNoteId, WalletDb,
|
||||
};
|
||||
|
||||
|
@ -65,9 +71,7 @@ use super::BlockDb;
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
zcash_client_backend::data_api::wallet::{propose_shielding, shield_transparent_funds},
|
||||
zcash_primitives::{
|
||||
legacy::TransparentAddress, transaction::components::amount::NonNegativeAmount,
|
||||
},
|
||||
zcash_primitives::legacy::TransparentAddress,
|
||||
};
|
||||
|
||||
#[cfg(feature = "unstable")]
|
||||
|
@ -286,6 +290,66 @@ where
|
|||
limit,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn get_total_balance(&self, account: AccountId) -> NonNegativeAmount {
|
||||
get_wallet_summary(&self.wallet().conn, 0, &SubtreeScanProgress)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.account_balances()
|
||||
.get(&account)
|
||||
.unwrap()
|
||||
.total()
|
||||
}
|
||||
|
||||
pub(crate) fn get_spendable_balance(
|
||||
&self,
|
||||
account: AccountId,
|
||||
min_confirmations: u32,
|
||||
) -> NonNegativeAmount {
|
||||
let binding =
|
||||
get_wallet_summary(&self.wallet().conn, min_confirmations, &SubtreeScanProgress)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let balance = binding.account_balances().get(&account).unwrap();
|
||||
|
||||
balance.sapling_balance.spendable_value
|
||||
}
|
||||
|
||||
pub(crate) fn get_pending_shielded_balance(
|
||||
&self,
|
||||
account: AccountId,
|
||||
min_confirmations: u32,
|
||||
) -> NonNegativeAmount {
|
||||
let binding =
|
||||
get_wallet_summary(&self.wallet().conn, min_confirmations, &SubtreeScanProgress)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let balance = binding.account_balances().get(&account).unwrap();
|
||||
|
||||
(balance.sapling_balance.value_pending_spendability
|
||||
+ balance.sapling_balance.change_pending_confirmation)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn get_pending_change(
|
||||
&self,
|
||||
account: AccountId,
|
||||
min_confirmations: u32,
|
||||
) -> NonNegativeAmount {
|
||||
let binding =
|
||||
get_wallet_summary(&self.wallet().conn, min_confirmations, &SubtreeScanProgress)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let balance = binding.account_balances().get(&account).unwrap();
|
||||
|
||||
balance.sapling_balance.change_pending_confirmation
|
||||
}
|
||||
|
||||
pub(crate) fn get_wallet_summary(&self, min_confirmations: u32) -> WalletSummary {
|
||||
get_wallet_summary(&self.wallet().conn, min_confirmations, &SubtreeScanProgress)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Cache> TestState<Cache> {
|
||||
|
|
|
@ -68,11 +68,13 @@ use incrementalmerkletree::Retention;
|
|||
use rusqlite::{self, named_params, OptionalExtension, ToSql};
|
||||
use shardtree::ShardTree;
|
||||
use std::cmp;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::convert::TryFrom;
|
||||
use std::io::{self, Cursor};
|
||||
use std::num::NonZeroU32;
|
||||
use tracing::debug;
|
||||
use zcash_client_backend::data_api::{AccountBalance, Balance, Ratio, WalletSummary};
|
||||
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
|
||||
|
||||
use zcash_client_backend::data_api::{
|
||||
scanning::{ScanPriority, ScanRange},
|
||||
|
@ -106,7 +108,7 @@ use crate::{
|
|||
};
|
||||
use crate::{SAPLING_TABLES_PREFIX, VERIFY_LOOKAHEAD};
|
||||
|
||||
use self::scanning::replace_queue_entries;
|
||||
use self::scanning::{parse_priority_code, replace_queue_entries};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
|
@ -519,28 +521,264 @@ pub(crate) fn get_balance(
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the verified balance for the account at the specified height,
|
||||
/// This may be used to obtain a balance that ignores notes that have been
|
||||
/// received so recently that they are not yet deemed spendable.
|
||||
pub(crate) fn get_balance_at(
|
||||
pub(crate) trait ScanProgress {
|
||||
fn sapling_scan_progress(
|
||||
&self,
|
||||
conn: &rusqlite::Connection,
|
||||
birthday_height: BlockHeight,
|
||||
fully_scanned_height: BlockHeight,
|
||||
chain_tip_height: BlockHeight,
|
||||
) -> Result<Option<Ratio<u64>>, SqliteClientError>;
|
||||
}
|
||||
|
||||
pub(crate) struct SubtreeScanProgress;
|
||||
|
||||
impl ScanProgress for SubtreeScanProgress {
|
||||
fn sapling_scan_progress(
|
||||
&self,
|
||||
conn: &rusqlite::Connection,
|
||||
birthday_height: BlockHeight,
|
||||
fully_scanned_height: BlockHeight,
|
||||
chain_tip_height: BlockHeight,
|
||||
) -> Result<Option<Ratio<u64>>, SqliteClientError> {
|
||||
if fully_scanned_height == chain_tip_height {
|
||||
// Compute the total blocks scanned since the wallet birthday
|
||||
conn.query_row(
|
||||
"SELECT SUM(sapling_output_count)
|
||||
FROM blocks
|
||||
WHERE height >= :birthday_height",
|
||||
named_params![":birthday_height": u32::from(birthday_height)],
|
||||
|row| {
|
||||
let scanned = row.get::<_, Option<u64>>(0)?;
|
||||
Ok(scanned.map(|n| Ratio::new(n, n)))
|
||||
},
|
||||
)
|
||||
.map_err(SqliteClientError::from)
|
||||
} else {
|
||||
// Compute the number of fully scanned notes directly from the blocks table
|
||||
let fully_scanned_size = conn.query_row(
|
||||
"SELECT MAX(sapling_commitment_tree_size)
|
||||
FROM blocks
|
||||
WHERE height <= :fully_scanned_height",
|
||||
named_params![":fully_scanned_height": u32::from(fully_scanned_height)],
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
)?;
|
||||
|
||||
// Compute the total blocks scanned so far above the fully scanned height
|
||||
let scanned_count = conn.query_row(
|
||||
"SELECT SUM(sapling_output_count)
|
||||
FROM blocks
|
||||
WHERE height > :fully_scanned_height",
|
||||
named_params![":fully_scanned_height": u32::from(fully_scanned_height)],
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
)?;
|
||||
|
||||
// We don't have complete information on how many outputs will exist in the shard at
|
||||
// the chain tip without having scanned the chain tip block, so we overestimate by
|
||||
// computing the maximum possible number of notes directly from the shard indices.
|
||||
//
|
||||
// TODO: it would be nice to be able to reliably have the size of the commitment tree
|
||||
// at the chain tip without having to have scanned that block.
|
||||
Ok(conn
|
||||
.query_row(
|
||||
"SELECT MIN(shard_index), MAX(shard_index)
|
||||
FROM sapling_tree_shards
|
||||
WHERE subtree_end_height > :fully_scanned_height
|
||||
OR subtree_end_height IS NULL",
|
||||
named_params![":fully_scanned_height": u32::from(fully_scanned_height)],
|
||||
|row| {
|
||||
let min_tree_size = row
|
||||
.get::<_, Option<u64>>(0)?
|
||||
.map(|min| min << SAPLING_SHARD_HEIGHT);
|
||||
let max_idx = row.get::<_, Option<u64>>(1)?;
|
||||
Ok(fully_scanned_size.or(min_tree_size).zip(max_idx).map(
|
||||
|(min_tree_size, max)| {
|
||||
let max_tree_size = (max + 1) << SAPLING_SHARD_HEIGHT;
|
||||
Ratio::new(
|
||||
scanned_count.unwrap_or(0),
|
||||
max_tree_size - min_tree_size,
|
||||
)
|
||||
},
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()?
|
||||
.flatten())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the spendable balance for the account at the specified height.
|
||||
///
|
||||
/// This may be used to obtain a balance that ignores notes that have been detected so recently
|
||||
/// that they are not yet spendable, or for which it is not yet possible to construct witnesses.
|
||||
pub(crate) fn get_wallet_summary(
|
||||
conn: &rusqlite::Connection,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Amount, SqliteClientError> {
|
||||
let balance = conn.query_row(
|
||||
"SELECT SUM(value) FROM sapling_received_notes
|
||||
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
|
||||
WHERE account = ? AND spent IS NULL AND transactions.block <= ?",
|
||||
[u32::from(account), u32::from(anchor_height)],
|
||||
|row| row.get(0).or(Ok(0)),
|
||||
min_confirmations: u32,
|
||||
progress: &impl ScanProgress,
|
||||
) -> Result<Option<WalletSummary>, SqliteClientError> {
|
||||
let chain_tip_height = match scan_queue_extrema(conn)? {
|
||||
Some((_, max)) => max,
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let birthday_height =
|
||||
wallet_birthday(conn)?.expect("If a scan range exists, we know the wallet birthday.");
|
||||
|
||||
let fully_scanned_height =
|
||||
block_fully_scanned(conn)?.map_or(birthday_height - 1, |m| m.block_height());
|
||||
let summary_height = chain_tip_height + 1 - min_confirmations;
|
||||
|
||||
let sapling_scan_progress = progress.sapling_scan_progress(
|
||||
conn,
|
||||
birthday_height,
|
||||
fully_scanned_height,
|
||||
chain_tip_height,
|
||||
)?;
|
||||
|
||||
match Amount::from_i64(balance) {
|
||||
Ok(amount) if !amount.is_negative() => Ok(amount),
|
||||
_ => Err(SqliteClientError::CorruptedData(
|
||||
"Sum of values in sapling_received_notes is out of range".to_string(),
|
||||
)),
|
||||
// If the shard containing the anchor is contains any unscanned ranges below the summary
|
||||
// height, none of our balance is currently spendable.
|
||||
let any_spendable = conn.query_row(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM v_sapling_shard_unscanned_ranges
|
||||
WHERE :summary_height
|
||||
BETWEEN subtree_start_height
|
||||
AND IFNULL(subtree_end_height, :summary_height)
|
||||
AND block_range_start <= :summary_height
|
||||
)",
|
||||
named_params![":summary_height": u32::from(summary_height)],
|
||||
|row| row.get::<_, bool>(0).map(|b| !b),
|
||||
)?;
|
||||
|
||||
let mut stmt_select_notes = conn.prepare_cached(
|
||||
"SELECT n.account, n.value, n.is_change, scan_state.max_priority, t.block, t.expiry_height
|
||||
FROM sapling_received_notes n
|
||||
JOIN transactions t ON t.id_tx = n.tx
|
||||
LEFT OUTER JOIN v_sapling_shards_scan_state scan_state
|
||||
ON n.commitment_tree_position >= scan_state.start_position
|
||||
AND n.commitment_tree_position < scan_state.end_position_exclusive
|
||||
WHERE n.spent IS NULL
|
||||
AND (
|
||||
t.expiry_height IS NULL
|
||||
OR t.block IS NOT NULL
|
||||
OR t.expiry_height >= :summary_height
|
||||
)",
|
||||
)?;
|
||||
|
||||
let mut account_balances: BTreeMap<AccountId, AccountBalance> = BTreeMap::new();
|
||||
let mut rows =
|
||||
stmt_select_notes.query(named_params![":summary_height": u32::from(summary_height)])?;
|
||||
while let Some(row) = rows.next()? {
|
||||
let account = row.get::<_, u32>(0).map(AccountId::from)?;
|
||||
|
||||
let value_raw = row.get::<_, i64>(1)?;
|
||||
let value = NonNegativeAmount::from_nonnegative_i64(value_raw).map_err(|_| {
|
||||
SqliteClientError::CorruptedData(format!("Negative received note value: {}", value_raw))
|
||||
})?;
|
||||
|
||||
let is_change = row.get::<_, bool>(2)?;
|
||||
|
||||
// If `max_priority` is null, this means that the note is not positioned; the note
|
||||
// will not be spendable, so we assign the scan priority to `ChainTip` as a priority
|
||||
// that is greater than `Scanned`
|
||||
let max_priority_raw = row.get::<_, Option<i64>>(3)?;
|
||||
let max_priority = max_priority_raw.map_or_else(
|
||||
|| Ok(ScanPriority::ChainTip),
|
||||
|raw| {
|
||||
parse_priority_code(raw).ok_or_else(|| {
|
||||
SqliteClientError::CorruptedData(format!(
|
||||
"Priority code {} not recognized.",
|
||||
raw
|
||||
))
|
||||
})
|
||||
},
|
||||
)?;
|
||||
|
||||
let received_height = row
|
||||
.get::<_, Option<u32>>(4)
|
||||
.map(|opt| opt.map(BlockHeight::from))?;
|
||||
|
||||
let is_spendable = any_spendable
|
||||
&& received_height.iter().any(|h| h <= &summary_height)
|
||||
&& max_priority <= ScanPriority::Scanned;
|
||||
|
||||
let is_pending_change = is_change && received_height.iter().all(|h| h > &summary_height);
|
||||
|
||||
let (spendable_value, change_pending_confirmation, value_pending_spendability) = {
|
||||
let zero = NonNegativeAmount::ZERO;
|
||||
if is_spendable {
|
||||
(value, zero, zero)
|
||||
} else if is_pending_change {
|
||||
(zero, value, zero)
|
||||
} else {
|
||||
(zero, zero, value)
|
||||
}
|
||||
};
|
||||
|
||||
account_balances
|
||||
.entry(account)
|
||||
.and_modify(|bal| {
|
||||
bal.sapling_balance.spendable_value = (bal.sapling_balance.spendable_value
|
||||
+ spendable_value)
|
||||
.expect("Spendable value cannot overflow");
|
||||
bal.sapling_balance.change_pending_confirmation =
|
||||
(bal.sapling_balance.change_pending_confirmation + change_pending_confirmation)
|
||||
.expect("Pending change value cannot overflow");
|
||||
bal.sapling_balance.value_pending_spendability =
|
||||
(bal.sapling_balance.value_pending_spendability + value_pending_spendability)
|
||||
.expect("Value pending spendability cannot overflow");
|
||||
})
|
||||
.or_insert(AccountBalance {
|
||||
sapling_balance: Balance {
|
||||
spendable_value,
|
||||
change_pending_confirmation,
|
||||
value_pending_spendability,
|
||||
},
|
||||
unshielded: NonNegativeAmount::ZERO,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
{
|
||||
let mut stmt_transparent_balances = conn.prepare(
|
||||
"SELECT u.received_by_account, SUM(u.value_zat)
|
||||
FROM utxos u
|
||||
LEFT OUTER JOIN transactions tx
|
||||
ON tx.id_tx = u.spent_in_tx
|
||||
WHERE u.height <= :max_height
|
||||
AND tx.block IS NULL
|
||||
GROUP BY u.received_by_account",
|
||||
)?;
|
||||
let mut rows = stmt_transparent_balances
|
||||
.query(named_params![":max_height": u32::from(summary_height)])?;
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
let account = AccountId::from(row.get::<_, u32>(0)?);
|
||||
let raw_value = row.get(1)?;
|
||||
let value = NonNegativeAmount::from_nonnegative_i64(raw_value).map_err(|_| {
|
||||
SqliteClientError::CorruptedData(format!("Negative UTXO value {:?}", raw_value))
|
||||
})?;
|
||||
|
||||
account_balances
|
||||
.entry(account)
|
||||
.and_modify(|bal| bal.unshielded = value)
|
||||
.or_insert(AccountBalance {
|
||||
sapling_balance: Balance::ZERO,
|
||||
unshielded: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let summary = WalletSummary::new(
|
||||
account_balances,
|
||||
chain_tip_height,
|
||||
fully_scanned_height,
|
||||
sapling_scan_progress,
|
||||
);
|
||||
|
||||
Ok(Some(summary))
|
||||
}
|
||||
|
||||
/// Returns the memo for a received note, if the note is known to the wallet.
|
||||
|
@ -828,52 +1066,50 @@ pub(crate) fn block_metadata(
|
|||
pub(crate) fn block_fully_scanned(
|
||||
conn: &rusqlite::Connection,
|
||||
) -> Result<Option<BlockMetadata>, SqliteClientError> {
|
||||
// We assume here that the wallet was either initialized via `init_blocks_table`, or
|
||||
// its birthday is Sapling activation, so the earliest block in the `blocks` table is
|
||||
// the first fully-scanned block (because it occurs before any wallet activity).
|
||||
//
|
||||
// We further assume that the only way we get a contiguous range of block heights in
|
||||
// the `blocks` table starting with this earliest block, is if all scanning operations
|
||||
// have been performed on those blocks. This holds because the `blocks` table is only
|
||||
// altered by `WalletDb::put_blocks` via `put_block`, and the effective combination of
|
||||
// intra-range linear scanning and the nullifier map ensures that we discover all
|
||||
// wallet-related information within the contiguous range.
|
||||
//
|
||||
// The fully-scanned height is therefore the greatest height in the first contiguous
|
||||
// range of block rows, which is a combined case of the "gaps and islands" and
|
||||
// "greatest N per group" SQL query problems.
|
||||
conn.query_row(
|
||||
"SELECT height, hash, sapling_commitment_tree_size, sapling_tree
|
||||
FROM blocks
|
||||
INNER JOIN (
|
||||
WITH contiguous AS (
|
||||
SELECT height, ROW_NUMBER() OVER (ORDER BY height) - height AS grp
|
||||
FROM blocks
|
||||
if let Some(birthday_height) = wallet_birthday(conn)? {
|
||||
// We assume that the only way we get a contiguous range of block heights in the `blocks` table
|
||||
// starting with the birthday block, is if all scanning operations have been performed on those
|
||||
// blocks. This holds because the `blocks` table is only altered by `WalletDb::put_blocks` via
|
||||
// `put_block`, and the effective combination of intra-range linear scanning and the nullifier
|
||||
// map ensures that we discover all wallet-related information within the contiguous range.
|
||||
//
|
||||
// The fully-scanned height is therefore the greatest height in the first contiguous range of
|
||||
// block rows, which is a combined case of the "gaps and islands" and "greatest N per group"
|
||||
// SQL query problems.
|
||||
conn.query_row(
|
||||
"SELECT height, hash, sapling_commitment_tree_size, sapling_tree
|
||||
FROM blocks
|
||||
INNER JOIN (
|
||||
WITH contiguous AS (
|
||||
SELECT height, ROW_NUMBER() OVER (ORDER BY height) - height AS grp
|
||||
FROM blocks
|
||||
)
|
||||
SELECT MIN(height) AS group_min_height, MAX(height) AS group_max_height
|
||||
FROM contiguous
|
||||
GROUP BY grp
|
||||
HAVING :birthday_height BETWEEN group_min_height AND group_max_height
|
||||
)
|
||||
SELECT MAX(height) AS [fully_scanned_height]
|
||||
FROM contiguous
|
||||
GROUP BY grp
|
||||
ORDER BY height
|
||||
LIMIT 1
|
||||
ON height = group_max_height",
|
||||
named_params![":birthday_height": u32::from(birthday_height)],
|
||||
|row| {
|
||||
let height: u32 = row.get(0)?;
|
||||
let block_hash: Vec<u8> = row.get(1)?;
|
||||
let sapling_tree_size: Option<u32> = row.get(2)?;
|
||||
let sapling_tree: Vec<u8> = row.get(3)?;
|
||||
Ok((
|
||||
BlockHeight::from(height),
|
||||
block_hash,
|
||||
sapling_tree_size,
|
||||
sapling_tree,
|
||||
))
|
||||
},
|
||||
)
|
||||
ON height = fully_scanned_height",
|
||||
[],
|
||||
|row| {
|
||||
let height: u32 = row.get(0)?;
|
||||
let block_hash: Vec<u8> = row.get(1)?;
|
||||
let sapling_tree_size: Option<u32> = row.get(2)?;
|
||||
let sapling_tree: Vec<u8> = row.get(3)?;
|
||||
Ok((
|
||||
BlockHeight::from(height),
|
||||
block_hash,
|
||||
sapling_tree_size,
|
||||
sapling_tree,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(SqliteClientError::from)
|
||||
.and_then(|meta_row| meta_row.map(parse_block_metadata).transpose())
|
||||
.optional()
|
||||
.map_err(SqliteClientError::from)
|
||||
.and_then(|meta_row| meta_row.map(parse_block_metadata).transpose())
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the block height at which the specified transaction was mined,
|
||||
|
@ -1171,6 +1407,7 @@ pub(crate) fn put_block(
|
|||
block_hash: BlockHash,
|
||||
block_time: u32,
|
||||
sapling_commitment_tree_size: u32,
|
||||
sapling_output_count: u32,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let block_hash_data = conn
|
||||
.query_row(
|
||||
|
@ -1200,6 +1437,7 @@ pub(crate) fn put_block(
|
|||
hash,
|
||||
time,
|
||||
sapling_commitment_tree_size,
|
||||
sapling_output_count,
|
||||
sapling_tree
|
||||
)
|
||||
VALUES (
|
||||
|
@ -1207,19 +1445,22 @@ pub(crate) fn put_block(
|
|||
:hash,
|
||||
:block_time,
|
||||
:sapling_commitment_tree_size,
|
||||
:sapling_output_count,
|
||||
x'00'
|
||||
)
|
||||
ON CONFLICT (height) DO UPDATE
|
||||
SET hash = :hash,
|
||||
time = :block_time,
|
||||
sapling_commitment_tree_size = :sapling_commitment_tree_size",
|
||||
sapling_commitment_tree_size = :sapling_commitment_tree_size,
|
||||
sapling_output_count = :sapling_output_count",
|
||||
)?;
|
||||
|
||||
stmt_upsert_block.execute(named_params![
|
||||
":height": u32::from(block_height),
|
||||
":hash": &block_hash.0[..],
|
||||
":block_time": block_time,
|
||||
":sapling_commitment_tree_size": sapling_commitment_tree_size
|
||||
":sapling_commitment_tree_size": sapling_commitment_tree_size,
|
||||
":sapling_output_count": sapling_output_count,
|
||||
])?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -219,7 +219,9 @@ mod tests {
|
|||
time INTEGER NOT NULL,
|
||||
sapling_tree BLOB NOT NULL ,
|
||||
sapling_commitment_tree_size INTEGER,
|
||||
orchard_commitment_tree_size INTEGER)",
|
||||
orchard_commitment_tree_size INTEGER,
|
||||
sapling_output_count INTEGER,
|
||||
orchard_action_count INTEGER)",
|
||||
"CREATE TABLE nullifier_map (
|
||||
spend_pool INTEGER NOT NULL,
|
||||
nf BLOB NOT NULL,
|
||||
|
@ -365,16 +367,15 @@ mod tests {
|
|||
}
|
||||
|
||||
let expected_views = vec![
|
||||
// v_sapling_shard_unscanned_ranges
|
||||
// v_sapling_shard_scan_ranges
|
||||
format!(
|
||||
"CREATE VIEW v_sapling_shard_unscanned_ranges AS
|
||||
WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts)
|
||||
"CREATE VIEW v_sapling_shard_scan_ranges AS
|
||||
SELECT
|
||||
shard.shard_index,
|
||||
shard.shard_index << 16 AS start_position,
|
||||
(shard.shard_index + 1) << 16 AS end_position_exclusive,
|
||||
IFNULL(prev_shard.subtree_end_height, {}) AS subtree_start_height,
|
||||
shard.subtree_end_height AS subtree_end_height,
|
||||
shard.subtree_end_height,
|
||||
shard.contains_marked,
|
||||
scan_queue.block_range_start,
|
||||
scan_queue.block_range_end,
|
||||
|
@ -383,18 +384,53 @@ mod tests {
|
|||
LEFT OUTER JOIN sapling_tree_shards prev_shard
|
||||
ON shard.shard_index = prev_shard.shard_index + 1
|
||||
INNER JOIN scan_queue ON
|
||||
(scan_queue.block_range_start >= subtree_start_height AND shard.subtree_end_height IS NULL) OR
|
||||
(scan_queue.block_range_start BETWEEN subtree_start_height AND shard.subtree_end_height) OR
|
||||
((scan_queue.block_range_end - 1) BETWEEN subtree_start_height AND shard.subtree_end_height) OR
|
||||
(
|
||||
scan_queue.block_range_start <= prev_shard.subtree_end_height
|
||||
AND (scan_queue.block_range_end - 1) >= shard.subtree_end_height
|
||||
)
|
||||
INNER JOIN wallet_birthday
|
||||
WHERE scan_queue.priority > {}
|
||||
AND scan_queue.block_range_end > wallet_birthday.height",
|
||||
)",
|
||||
u32::from(st.network().activation_height(NetworkUpgrade::Sapling).unwrap()),
|
||||
),
|
||||
// v_sapling_shard_unscanned_ranges
|
||||
format!(
|
||||
"CREATE VIEW v_sapling_shard_unscanned_ranges AS
|
||||
WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts)
|
||||
SELECT
|
||||
shard_index,
|
||||
start_position,
|
||||
end_position_exclusive,
|
||||
subtree_start_height,
|
||||
subtree_end_height,
|
||||
contains_marked,
|
||||
block_range_start,
|
||||
block_range_end,
|
||||
priority
|
||||
FROM v_sapling_shard_scan_ranges
|
||||
INNER JOIN wallet_birthday
|
||||
WHERE priority > {}
|
||||
AND block_range_end > wallet_birthday.height",
|
||||
priority_code(&ScanPriority::Scanned)
|
||||
),
|
||||
// v_sapling_shards_scan_state
|
||||
"CREATE VIEW v_sapling_shards_scan_state AS
|
||||
SELECT
|
||||
shard_index,
|
||||
start_position,
|
||||
end_position_exclusive,
|
||||
subtree_start_height,
|
||||
subtree_end_height,
|
||||
contains_marked,
|
||||
MAX(priority) AS max_priority
|
||||
FROM v_sapling_shard_scan_ranges
|
||||
GROUP BY
|
||||
shard_index,
|
||||
start_position,
|
||||
end_position_exclusive,
|
||||
subtree_start_height,
|
||||
subtree_end_height,
|
||||
contains_marked".to_owned(),
|
||||
// v_transactions
|
||||
"CREATE VIEW v_transactions AS
|
||||
WITH
|
||||
|
|
|
@ -12,6 +12,7 @@ mod ufvk_support;
|
|||
mod utxos_table;
|
||||
mod v_sapling_shard_unscanned_ranges;
|
||||
mod v_transactions_net;
|
||||
mod wallet_summaries;
|
||||
|
||||
use schemer_rusqlite::RusqliteMigration;
|
||||
use secrecy::SecretVec;
|
||||
|
@ -42,6 +43,8 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
|
|||
// add_account_birthdays
|
||||
// |
|
||||
// v_sapling_shard_unscanned_ranges
|
||||
// |
|
||||
// wallet_summaries
|
||||
vec![
|
||||
Box::new(initial_setup::Migration {}),
|
||||
Box::new(utxos_table::Migration {}),
|
||||
|
@ -72,5 +75,6 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
|
|||
Box::new(v_sapling_shard_unscanned_ranges::Migration {
|
||||
params: params.clone(),
|
||||
}),
|
||||
Box::new(wallet_summaries::Migration),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -38,14 +38,13 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
|
||||
transaction.execute_batch(
|
||||
&format!(
|
||||
"CREATE VIEW v_sapling_shard_unscanned_ranges AS
|
||||
WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts)
|
||||
"CREATE VIEW v_sapling_shard_scan_ranges AS
|
||||
SELECT
|
||||
shard.shard_index,
|
||||
shard.shard_index << {} AS start_position,
|
||||
(shard.shard_index + 1) << {} AS end_position_exclusive,
|
||||
IFNULL(prev_shard.subtree_end_height, {}) AS subtree_start_height,
|
||||
shard.subtree_end_height AS subtree_end_height,
|
||||
shard.subtree_end_height,
|
||||
shard.contains_marked,
|
||||
scan_queue.block_range_start,
|
||||
scan_queue.block_range_end,
|
||||
|
@ -54,22 +53,39 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
LEFT OUTER JOIN sapling_tree_shards prev_shard
|
||||
ON shard.shard_index = prev_shard.shard_index + 1
|
||||
INNER JOIN scan_queue ON
|
||||
(scan_queue.block_range_start >= subtree_start_height AND shard.subtree_end_height IS NULL) OR
|
||||
(scan_queue.block_range_start BETWEEN subtree_start_height AND shard.subtree_end_height) OR
|
||||
((scan_queue.block_range_end - 1) BETWEEN subtree_start_height AND shard.subtree_end_height) OR
|
||||
(
|
||||
scan_queue.block_range_start <= prev_shard.subtree_end_height
|
||||
AND (scan_queue.block_range_end - 1) >= shard.subtree_end_height
|
||||
)
|
||||
INNER JOIN wallet_birthday
|
||||
WHERE scan_queue.priority > {}
|
||||
AND scan_queue.block_range_end > wallet_birthday.height;",
|
||||
)",
|
||||
SAPLING_SHARD_HEIGHT,
|
||||
SAPLING_SHARD_HEIGHT,
|
||||
u32::from(self.params.activation_height(NetworkUpgrade::Sapling).unwrap()),
|
||||
priority_code(&ScanPriority::Scanned),
|
||||
)
|
||||
)?;
|
||||
|
||||
transaction.execute_batch(&format!(
|
||||
"CREATE VIEW v_sapling_shard_unscanned_ranges AS
|
||||
WITH wallet_birthday AS (SELECT MIN(birthday_height) AS height FROM accounts)
|
||||
SELECT
|
||||
shard_index,
|
||||
start_position,
|
||||
end_position_exclusive,
|
||||
subtree_start_height,
|
||||
subtree_end_height,
|
||||
contains_marked,
|
||||
block_range_start,
|
||||
block_range_end,
|
||||
priority
|
||||
FROM v_sapling_shard_scan_ranges
|
||||
INNER JOIN wallet_birthday
|
||||
WHERE priority > {}
|
||||
AND block_range_end > wallet_birthday.height;",
|
||||
priority_code(&ScanPriority::Scanned),
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
//! This migration adds views and database changes required to provide accurate wallet summaries.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use schemer_rusqlite::RusqliteMigration;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::wallet::init::WalletMigrationError;
|
||||
|
||||
use super::v_sapling_shard_unscanned_ranges;
|
||||
|
||||
pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xc5bf7f71_2297_41ff_89e1_75e07c4e8838);
|
||||
|
||||
pub(super) struct Migration;
|
||||
|
||||
impl schemer::Migration for Migration {
|
||||
fn id(&self) -> Uuid {
|
||||
MIGRATION_ID
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> HashSet<Uuid> {
|
||||
[v_sapling_shard_unscanned_ranges::MIGRATION_ID]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Adds views and data required to produce accurate wallet summaries."
|
||||
}
|
||||
}
|
||||
|
||||
impl RusqliteMigration for Migration {
|
||||
type Error = WalletMigrationError;
|
||||
|
||||
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
|
||||
// Add columns to the `blocks` table to track the number of scanned outputs in each block.
|
||||
// We use the note commitment tree size information that we have in contiguous regions to
|
||||
// populate this data, but we don't make any attempt to handle the boundary cases because
|
||||
// we're just using this information for the progress metric, which can be a bit sloppy.
|
||||
transaction.execute_batch(
|
||||
"ALTER TABLE blocks ADD COLUMN sapling_output_count INTEGER;
|
||||
ALTER TABLE blocks ADD COLUMN orchard_action_count INTEGER;",
|
||||
)?;
|
||||
|
||||
transaction.execute_batch(
|
||||
// set the number of outputs everywhere that we have sequential Sapling blocks
|
||||
"CREATE TEMPORARY TABLE block_deltas AS
|
||||
SELECT
|
||||
cur.height AS height,
|
||||
(cur.sapling_commitment_tree_size - prev.sapling_commitment_tree_size) AS sapling_delta,
|
||||
(cur.orchard_commitment_tree_size - prev.orchard_commitment_tree_size) AS orchard_delta
|
||||
FROM blocks cur
|
||||
INNER JOIN blocks prev
|
||||
ON cur.height = prev.height + 1;
|
||||
|
||||
UPDATE blocks
|
||||
SET sapling_output_count = block_deltas.sapling_delta,
|
||||
orchard_action_count = block_deltas.orchard_delta
|
||||
FROM block_deltas
|
||||
WHERE block_deltas.height = blocks.height;"
|
||||
)?;
|
||||
|
||||
transaction.execute_batch(
|
||||
"CREATE VIEW v_sapling_shards_scan_state AS
|
||||
SELECT
|
||||
shard_index,
|
||||
start_position,
|
||||
end_position_exclusive,
|
||||
subtree_start_height,
|
||||
subtree_end_height,
|
||||
contains_marked,
|
||||
MAX(priority) AS max_priority
|
||||
FROM v_sapling_shard_scan_ranges
|
||||
GROUP BY
|
||||
shard_index,
|
||||
start_position,
|
||||
end_position_exclusive,
|
||||
subtree_start_height,
|
||||
subtree_end_height,
|
||||
contains_marked;",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
|
||||
panic!("This migration cannot be reverted.");
|
||||
}
|
||||
}
|
|
@ -437,7 +437,10 @@ pub(crate) mod tests {
|
|||
note_encryption::try_sapling_output_recovery, prover::TxProver, Note, PaymentAddress,
|
||||
},
|
||||
transaction::{
|
||||
components::{amount::BalanceError, Amount},
|
||||
components::{
|
||||
amount::{BalanceError, NonNegativeAmount},
|
||||
Amount,
|
||||
},
|
||||
fees::{fixed::FeeRule as FixedFeeRule, zip317::FeeRule as Zip317FeeRule},
|
||||
Transaction,
|
||||
},
|
||||
|
@ -450,7 +453,7 @@ pub(crate) mod tests {
|
|||
self,
|
||||
error::Error,
|
||||
wallet::input_selection::{GreedyInputSelector, GreedyInputSelectorError},
|
||||
AccountBirthday, ShieldedProtocol, WalletRead,
|
||||
AccountBirthday, Ratio, ShieldedProtocol, WalletRead,
|
||||
},
|
||||
decrypt_transaction,
|
||||
fees::{fixed, zip317, DustOutputPolicy},
|
||||
|
@ -462,14 +465,14 @@ pub(crate) mod tests {
|
|||
use crate::{
|
||||
error::SqliteClientError,
|
||||
testing::{AddressType, BlockCache, TestBuilder, TestState},
|
||||
wallet::{commitment_tree, get_balance, get_balance_at},
|
||||
wallet::{commitment_tree, get_balance},
|
||||
AccountId, NoteId, ReceivedNoteId,
|
||||
};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
zcash_client_backend::{data_api::WalletWrite, wallet::WalletTransparentOutput},
|
||||
zcash_primitives::transaction::components::{amount::NonNegativeAmount, OutPoint, TxOut},
|
||||
zcash_primitives::transaction::components::{OutPoint, TxOut},
|
||||
};
|
||||
|
||||
pub(crate) fn test_prover() -> impl TxProver {
|
||||
|
@ -497,18 +500,13 @@ pub(crate) mod tests {
|
|||
st.scan_cached_blocks(h, 1);
|
||||
|
||||
// Verified balance matches total balance
|
||||
let (_, anchor_height) = st
|
||||
.wallet()
|
||||
.get_target_and_anchor_heights(NonZeroU32::new(1).unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
get_balance(&st.wallet().conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
assert_eq!(
|
||||
get_balance_at(&st.wallet().conn, AccountId::from(0), anchor_height).unwrap(),
|
||||
value
|
||||
st.get_total_balance(account),
|
||||
NonNegativeAmount::try_from(value).unwrap()
|
||||
);
|
||||
|
||||
let to_extsk = ExtendedSpendingKey::master(&[]);
|
||||
|
@ -693,47 +691,45 @@ pub(crate) mod tests {
|
|||
.with_test_account(AccountBirthday::from_sapling_activation)
|
||||
.build();
|
||||
|
||||
let (_, usk, _) = st.test_account().unwrap();
|
||||
let (account, usk, _) = st.test_account().unwrap();
|
||||
let dfvk = st.test_account_sapling().unwrap();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
let value = Amount::from_u64(50000).unwrap();
|
||||
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
|
||||
let value = NonNegativeAmount::from_u64(50000).unwrap();
|
||||
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||
st.scan_cached_blocks(h1, 1);
|
||||
|
||||
// Verified balance matches total balance
|
||||
let (_, anchor_height) = st
|
||||
.wallet()
|
||||
.get_target_and_anchor_heights(NonZeroU32::new(10).unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
get_balance(&st.wallet().conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
assert_eq!(
|
||||
get_balance_at(&st.wallet().conn, AccountId::from(0), anchor_height).unwrap(),
|
||||
value
|
||||
get_balance(&st.wallet().conn, account).unwrap(),
|
||||
value.into()
|
||||
);
|
||||
assert_eq!(st.get_total_balance(account), value);
|
||||
|
||||
// Value is considered pending
|
||||
assert_eq!(st.get_pending_shielded_balance(account, 10), value);
|
||||
|
||||
// Wallet is fully scanned
|
||||
let summary = st.get_wallet_summary(1);
|
||||
assert_eq!(summary.sapling_scan_progress(), Some(Ratio::new(1, 1)));
|
||||
|
||||
// Add more funds to the wallet in a second note
|
||||
let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
|
||||
let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||
st.scan_cached_blocks(h2, 1);
|
||||
|
||||
// Verified balance does not include the second note
|
||||
let (_, anchor_height2) = st
|
||||
.wallet()
|
||||
.get_target_and_anchor_heights(NonZeroU32::new(10).unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let total = (value + value).unwrap();
|
||||
assert_eq!(
|
||||
get_balance(&st.wallet().conn, AccountId::from(0)).unwrap(),
|
||||
(value + value).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
get_balance_at(&st.wallet().conn, AccountId::from(0), anchor_height2).unwrap(),
|
||||
value
|
||||
get_balance(&st.wallet().conn, account).unwrap(),
|
||||
total.into()
|
||||
);
|
||||
assert_eq!(st.get_spendable_balance(account, 2), value);
|
||||
assert_eq!(st.get_pending_shielded_balance(account, 2), value);
|
||||
assert_eq!(st.get_total_balance(account), total);
|
||||
|
||||
// Wallet is still fully scanned
|
||||
let summary = st.get_wallet_summary(1);
|
||||
assert_eq!(summary.sapling_scan_progress(), Some(Ratio::new(2, 2)));
|
||||
|
||||
// Spend fails because there are insufficient verified notes
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
|
@ -758,7 +754,7 @@ pub(crate) mod tests {
|
|||
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second
|
||||
// note is verified
|
||||
for _ in 2..10 {
|
||||
st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
|
||||
st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||
}
|
||||
st.scan_cached_blocks(h2 + 1, 8);
|
||||
|
||||
|
@ -781,7 +777,7 @@ pub(crate) mod tests {
|
|||
);
|
||||
|
||||
// Mine block 11 so that the second note becomes verified
|
||||
let (h11, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
|
||||
let (h11, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||
st.scan_cached_blocks(h11, 1);
|
||||
|
||||
// Second spend should now succeed
|
||||
|
@ -805,17 +801,14 @@ pub(crate) mod tests {
|
|||
.with_test_account(AccountBirthday::from_sapling_activation)
|
||||
.build();
|
||||
|
||||
let (_, usk, _) = st.test_account().unwrap();
|
||||
let (account, usk, _) = st.test_account().unwrap();
|
||||
let dfvk = st.test_account_sapling().unwrap();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
let value = Amount::from_u64(50000).unwrap();
|
||||
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
|
||||
st.scan_cached_blocks(h1, 1);
|
||||
assert_eq!(
|
||||
get_balance(&st.wallet().conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
assert_eq!(get_balance(&st.wallet().conn, account).unwrap(), value);
|
||||
|
||||
// Send some of the funds to another address
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
|
@ -904,17 +897,14 @@ pub(crate) mod tests {
|
|||
.with_test_account(AccountBirthday::from_sapling_activation)
|
||||
.build();
|
||||
|
||||
let (_, usk, _) = st.test_account().unwrap();
|
||||
let (account, usk, _) = st.test_account().unwrap();
|
||||
let dfvk = st.test_account_sapling().unwrap();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
let value = Amount::from_u64(50000).unwrap();
|
||||
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
|
||||
st.scan_cached_blocks(h1, 1);
|
||||
assert_eq!(
|
||||
get_balance(&st.wallet().conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
assert_eq!(get_balance(&st.wallet().conn, account).unwrap(), value);
|
||||
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let addr2 = extsk2.default_address().1;
|
||||
|
@ -1005,7 +995,7 @@ pub(crate) mod tests {
|
|||
.with_test_account(AccountBirthday::from_sapling_activation)
|
||||
.build();
|
||||
|
||||
let (_, usk, _) = st.test_account().unwrap();
|
||||
let (account, usk, _) = st.test_account().unwrap();
|
||||
let dfvk = st.test_account_sapling().unwrap();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
|
@ -1014,18 +1004,10 @@ pub(crate) mod tests {
|
|||
st.scan_cached_blocks(h, 1);
|
||||
|
||||
// Verified balance matches total balance
|
||||
let (_, anchor_height) = st
|
||||
.wallet()
|
||||
.get_target_and_anchor_heights(NonZeroU32::new(1).unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(get_balance(&st.wallet().conn, account).unwrap(), value);
|
||||
assert_eq!(
|
||||
get_balance(&st.wallet().conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
assert_eq!(
|
||||
get_balance_at(&st.wallet().conn, AccountId::from(0), anchor_height).unwrap(),
|
||||
value
|
||||
st.get_total_balance(account),
|
||||
NonNegativeAmount::try_from(value).unwrap()
|
||||
);
|
||||
|
||||
let to = TransparentAddress::PublicKey([7; 20]).into();
|
||||
|
@ -1049,27 +1031,25 @@ pub(crate) mod tests {
|
|||
.with_test_account(AccountBirthday::from_sapling_activation)
|
||||
.build();
|
||||
|
||||
let (_, usk, _) = st.test_account().unwrap();
|
||||
let (account, usk, _) = st.test_account().unwrap();
|
||||
let dfvk = st.test_account_sapling().unwrap();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
// Add funds to the wallet in a single note owned by the internal spending key
|
||||
let value = Amount::from_u64(60000).unwrap();
|
||||
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::Internal, value);
|
||||
st.scan_cached_blocks(h, 1);
|
||||
|
||||
// Verified balance matches total balance
|
||||
let (_, anchor_height) = st
|
||||
.wallet()
|
||||
.get_target_and_anchor_heights(NonZeroU32::new(10).unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(get_balance(&st.wallet().conn, account).unwrap(), value);
|
||||
assert_eq!(
|
||||
get_balance(&st.wallet().conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
st.get_total_balance(account),
|
||||
NonNegativeAmount::try_from(value).unwrap()
|
||||
);
|
||||
|
||||
// the balance is considered pending
|
||||
assert_eq!(
|
||||
get_balance_at(&st.wallet().conn, AccountId::from(0), anchor_height).unwrap(),
|
||||
value
|
||||
st.get_pending_shielded_balance(account, 10),
|
||||
NonNegativeAmount::try_from(value).unwrap()
|
||||
);
|
||||
|
||||
let to = TransparentAddress::PublicKey([7; 20]).into();
|
||||
|
@ -1093,7 +1073,7 @@ pub(crate) mod tests {
|
|||
.with_test_account(AccountBirthday::from_sapling_activation)
|
||||
.build();
|
||||
|
||||
let (_, usk, _) = st.test_account().unwrap();
|
||||
let (account, usk, _) = st.test_account().unwrap();
|
||||
let dfvk = st.test_account_sapling().unwrap();
|
||||
|
||||
// Add funds to the wallet
|
||||
|
@ -1116,18 +1096,10 @@ pub(crate) mod tests {
|
|||
|
||||
// Verified balance matches total balance
|
||||
let total = Amount::from_u64(60000).unwrap();
|
||||
let (_, anchor_height) = st
|
||||
.wallet()
|
||||
.get_target_and_anchor_heights(NonZeroU32::new(1).unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(get_balance(&st.wallet().conn, account).unwrap(), total);
|
||||
assert_eq!(
|
||||
get_balance(&st.wallet().conn, AccountId::from(0)).unwrap(),
|
||||
total
|
||||
);
|
||||
assert_eq!(
|
||||
get_balance_at(&st.wallet().conn, AccountId::from(0), anchor_height).unwrap(),
|
||||
total
|
||||
st.get_total_balance(account),
|
||||
NonNegativeAmount::try_from(total).unwrap()
|
||||
);
|
||||
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
|
|
|
@ -881,7 +881,8 @@ mod tests {
|
|||
use zcash_client_backend::data_api::{
|
||||
chain::CommitmentTreeRoot,
|
||||
scanning::{ScanPriority, ScanRange},
|
||||
AccountBirthday, WalletCommitmentTrees, WalletRead, WalletWrite,
|
||||
AccountBirthday, Ratio, WalletCommitmentTrees, WalletRead, WalletWrite,
|
||||
SAPLING_SHARD_HEIGHT,
|
||||
};
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
|
@ -1689,6 +1690,10 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
// We have scan ranges and a subtree, but have scanned no blocks.
|
||||
let summary = st.get_wallet_summary(1);
|
||||
assert_eq!(summary.sapling_scan_progress(), None);
|
||||
|
||||
// Set up prior chain state. This simulates us having imported a wallet
|
||||
// with a birthday 520 blocks below the chain tip.
|
||||
st.wallet_mut().update_chain_tip(prior_tip).unwrap();
|
||||
|
@ -1723,6 +1728,14 @@ mod tests {
|
|||
);
|
||||
st.scan_cached_blocks(max_scanned, 1);
|
||||
|
||||
// We have scanned a block, so we now have a starting tree position, 500 blocks above the
|
||||
// wallet birthday but before the end of the shard.
|
||||
let summary = st.get_wallet_summary(1);
|
||||
assert_eq!(
|
||||
summary.sapling_scan_progress(),
|
||||
Some(Ratio::new(1, 0x1 << SAPLING_SHARD_HEIGHT))
|
||||
);
|
||||
|
||||
// Now simulate shutting down, and then restarting 70 blocks later, after a shard
|
||||
// has been completed.
|
||||
let last_shard_start = prior_tip + 50;
|
||||
|
@ -1758,6 +1771,14 @@ mod tests {
|
|||
|
||||
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
// We've crossed a subtree boundary, and so still only have one scanned note but have two
|
||||
// shards worth of notes to scan.
|
||||
let summary = st.get_wallet_summary(1);
|
||||
assert_eq!(
|
||||
summary.sapling_scan_progress(),
|
||||
Some(Ratio::new(1, 0x1 << (SAPLING_SHARD_HEIGHT + 1)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -16,6 +16,11 @@ and this library adheres to Rust's notion of
|
|||
- `impl HashSer for String` is provided under the `test-dependencies` feature
|
||||
flag. This is a test-only impl; the identity leaf value is `_` and the combining
|
||||
operation is concatenation.
|
||||
- `zcash_primitives::transaction::components::amount::NonNegativeAmount::ZERO`
|
||||
- Additional trait implementations for `NonNegativeAmount`:
|
||||
- `TryFrom<Amount> for NonNegativeAmount`
|
||||
- `Add<NonNegativeAmount> for NonNegativeAmount`
|
||||
- `Add<NonNegativeAmount> for Option<NonNegativeAmount>`
|
||||
|
||||
### Changed
|
||||
- `zcash_primitives::transaction`:
|
||||
|
|
|
@ -238,6 +238,9 @@ impl TryFrom<orchard::ValueSum> for Amount {
|
|||
pub struct NonNegativeAmount(Amount);
|
||||
|
||||
impl NonNegativeAmount {
|
||||
/// Returns the identity `NonNegativeAmount`
|
||||
pub const ZERO: Self = NonNegativeAmount(Amount(0));
|
||||
|
||||
/// Creates a NonNegativeAmount from a u64.
|
||||
///
|
||||
/// Returns an error if the amount is outside the range `{0..MAX_MONEY}`.
|
||||
|
@ -259,6 +262,34 @@ impl From<NonNegativeAmount> for Amount {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Amount> for NonNegativeAmount {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Amount) -> Result<Self, Self::Error> {
|
||||
if value.is_negative() {
|
||||
Err(())
|
||||
} else {
|
||||
Ok(NonNegativeAmount(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<NonNegativeAmount> for NonNegativeAmount {
|
||||
type Output = Option<NonNegativeAmount>;
|
||||
|
||||
fn add(self, rhs: NonNegativeAmount) -> Option<NonNegativeAmount> {
|
||||
(self.0 + rhs.0).map(NonNegativeAmount)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<NonNegativeAmount> for Option<NonNegativeAmount> {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: NonNegativeAmount) -> Option<NonNegativeAmount> {
|
||||
self.and_then(|lhs| lhs + rhs)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type for balance violations in amount addition and subtraction
|
||||
/// (overflow and underflow of allowed ranges)
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
|
|
Loading…
Reference in New Issue