Merge pull request #1541 from zcash/1070-zcb-wallet-summary-progress
Improve progress representation in `WalletSummary`
This commit is contained in:
commit
8b5236a45a
|
@ -7,10 +7,18 @@ and this library adheres to Rust's notion of
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- `zcash_client_backend::data_api`:
|
||||
- `WalletSummary::recovery_progress`
|
||||
|
||||
### Changed
|
||||
- The `Account` trait now uses an associated type for its `AccountId`
|
||||
type instead of a type parameter. This change allows for the simplification
|
||||
of some type signatures.
|
||||
- `zcash_client_backend::data_api`:
|
||||
- `WalletSummary::scan_progress` now only reports progress for scanning blocks
|
||||
"near" the chain tip. Progress for scanning earlier blocks is now reported
|
||||
via `WalletSummary::recovery_progress`.
|
||||
- `zcash_client_backend::sync::run`:
|
||||
- Transparent outputs are now refreshed in addition to shielded notes.
|
||||
|
||||
|
|
|
@ -471,6 +471,7 @@ pub struct WalletSummary<AccountId: Eq + Hash> {
|
|||
chain_tip_height: BlockHeight,
|
||||
fully_scanned_height: BlockHeight,
|
||||
scan_progress: Option<Ratio<u64>>,
|
||||
recovery_progress: Option<Ratio<u64>>,
|
||||
next_sapling_subtree_index: u64,
|
||||
#[cfg(feature = "orchard")]
|
||||
next_orchard_subtree_index: u64,
|
||||
|
@ -483,6 +484,7 @@ impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
|
|||
chain_tip_height: BlockHeight,
|
||||
fully_scanned_height: BlockHeight,
|
||||
scan_progress: Option<Ratio<u64>>,
|
||||
recovery_progress: Option<Ratio<u64>>,
|
||||
next_sapling_subtree_index: u64,
|
||||
#[cfg(feature = "orchard")] next_orchard_subtree_index: u64,
|
||||
) -> Self {
|
||||
|
@ -491,6 +493,7 @@ impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
|
|||
chain_tip_height,
|
||||
fully_scanned_height,
|
||||
scan_progress,
|
||||
recovery_progress,
|
||||
next_sapling_subtree_index,
|
||||
#[cfg(feature = "orchard")]
|
||||
next_orchard_subtree_index,
|
||||
|
@ -513,16 +516,47 @@ impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
|
|||
self.fully_scanned_height
|
||||
}
|
||||
|
||||
/// Returns the progress of scanning shielded outputs, in terms of the ratio between notes
|
||||
/// scanned and the total number of notes added to the chain since the wallet birthday.
|
||||
/// Returns the progress of scanning the chain to bring the wallet up to date.
|
||||
///
|
||||
/// 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.
|
||||
/// This progress metric is intended as an indicator of how close the wallet is to
|
||||
/// general usability, including the ability to spend existing funds that were
|
||||
/// previously spendable.
|
||||
///
|
||||
/// The window over which progress is computed spans from the wallet's recovery height
|
||||
/// to the current chain tip. This may be adjusted in future updates to better match
|
||||
/// the intended semantics.
|
||||
///
|
||||
/// Progress is represented in terms of the ratio between notes scanned and the total
|
||||
/// number of notes added to the chain in the relevant window. 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 scan_progress(&self) -> Option<Ratio<u64>> {
|
||||
self.scan_progress
|
||||
}
|
||||
|
||||
/// Returns the progress of recovering the wallet from seed.
|
||||
///
|
||||
/// This progress metric is intended as an indicator of how close the wallet is to
|
||||
/// having a complete history.
|
||||
///
|
||||
/// The window over which progress is computed spans from the wallet birthday to the
|
||||
/// wallet's recovery height. This may be adjusted in future updates to better match
|
||||
/// the intended semantics.
|
||||
///
|
||||
/// Progress is represented in terms of the ratio between notes scanned and the total
|
||||
/// number of notes added to the chain in the relevant window. 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 recovery_progress(&self) -> Option<Ratio<u64>> {
|
||||
self.recovery_progress
|
||||
}
|
||||
|
||||
/// Returns the Sapling subtree index that should start the next range of subtree
|
||||
/// roots passed to [`WalletCommitmentTrees::put_sapling_subtree_roots`].
|
||||
pub fn next_sapling_subtree_index(&self) -> u64 {
|
||||
|
|
|
@ -74,7 +74,7 @@ use zip32::fingerprint::SeedFingerprint;
|
|||
|
||||
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
|
||||
|
||||
#[cfg(any(feature = "test-dependencies", not(feature = "orchard")))]
|
||||
#[cfg(any(test, feature = "test-dependencies", not(feature = "orchard")))]
|
||||
use zcash_protocol::PoolType;
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
|
|
|
@ -686,8 +686,21 @@ pub(crate) fn spend_fails_on_unverified_notes<T: ShieldedPoolTester>() {
|
|||
NonNegativeAmount::ZERO
|
||||
);
|
||||
|
||||
// The account is configured without a recover-until height, so is by definition
|
||||
// fully recovered, and we count 1 per pool for both numerator and denominator.
|
||||
let fully_recovered = {
|
||||
let n = 1;
|
||||
#[cfg(feature = "orchard")]
|
||||
let n = n * 2;
|
||||
Some(Ratio::new(n, n))
|
||||
};
|
||||
|
||||
// Wallet is fully scanned
|
||||
let summary = st.get_wallet_summary(1);
|
||||
assert_eq!(
|
||||
summary.as_ref().and_then(|s| s.recovery_progress()),
|
||||
fully_recovered,
|
||||
);
|
||||
assert_eq!(
|
||||
summary.and_then(|s| s.scan_progress()),
|
||||
Some(Ratio::new(1, 1))
|
||||
|
@ -705,6 +718,10 @@ pub(crate) fn spend_fails_on_unverified_notes<T: ShieldedPoolTester>() {
|
|||
|
||||
// Wallet is still fully scanned
|
||||
let summary = st.get_wallet_summary(1);
|
||||
assert_eq!(
|
||||
summary.as_ref().and_then(|s| s.recovery_progress()),
|
||||
fully_recovered
|
||||
);
|
||||
assert_eq!(
|
||||
summary.and_then(|s| s.scan_progress()),
|
||||
Some(Ratio::new(2, 2))
|
||||
|
|
|
@ -808,213 +808,449 @@ pub(crate) fn get_derived_account<P: consensus::Parameters>(
|
|||
accounts.next().transpose()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Progress {
|
||||
scan: Option<Ratio<u64>>,
|
||||
recover: Option<Ratio<u64>>,
|
||||
}
|
||||
|
||||
pub(crate) trait ScanProgress {
|
||||
fn sapling_scan_progress(
|
||||
fn sapling_scan_progress<P: consensus::Parameters>(
|
||||
&self,
|
||||
conn: &rusqlite::Connection,
|
||||
params: &P,
|
||||
birthday_height: BlockHeight,
|
||||
recover_until_height: Option<BlockHeight>,
|
||||
fully_scanned_height: BlockHeight,
|
||||
chain_tip_height: BlockHeight,
|
||||
) -> Result<Option<Ratio<u64>>, SqliteClientError>;
|
||||
) -> Result<Progress, SqliteClientError>;
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
fn orchard_scan_progress(
|
||||
fn orchard_scan_progress<P: consensus::Parameters>(
|
||||
&self,
|
||||
conn: &rusqlite::Connection,
|
||||
params: &P,
|
||||
birthday_height: BlockHeight,
|
||||
recover_until_height: Option<BlockHeight>,
|
||||
fully_scanned_height: BlockHeight,
|
||||
chain_tip_height: BlockHeight,
|
||||
) -> Result<Option<Ratio<u64>>, SqliteClientError>;
|
||||
) -> Result<Progress, SqliteClientError>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SubtreeScanProgress;
|
||||
|
||||
impl ScanProgress for SubtreeScanProgress {
|
||||
#[tracing::instrument(skip(conn))]
|
||||
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 {
|
||||
// Get the starting note commitment tree size from the wallet birthday, or failing that
|
||||
// from the blocks table.
|
||||
let start_size = conn
|
||||
.query_row(
|
||||
"SELECT birthday_sapling_tree_size
|
||||
FROM accounts
|
||||
WHERE birthday_height = :birthday_height",
|
||||
named_params![":birthday_height": u32::from(birthday_height)],
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn subtree_scan_progress<P: consensus::Parameters>(
|
||||
conn: &rusqlite::Connection,
|
||||
params: &P,
|
||||
table_prefix: &'static str,
|
||||
output_count_col: &'static str,
|
||||
shard_height: u8,
|
||||
pool_activation_height: BlockHeight,
|
||||
birthday_height: BlockHeight,
|
||||
recover_until_height: Option<BlockHeight>,
|
||||
fully_scanned_height: BlockHeight,
|
||||
chain_tip_height: BlockHeight,
|
||||
) -> Result<Progress, SqliteClientError> {
|
||||
let mut stmt_scanned_count_between = conn.prepare_cached(&format!(
|
||||
"SELECT SUM({output_count_col})
|
||||
FROM blocks
|
||||
WHERE :start_height <= height AND height <= :end_height",
|
||||
))?;
|
||||
let mut stmt_scanned_count_from = conn.prepare_cached(&format!(
|
||||
"SELECT SUM({output_count_col})
|
||||
FROM blocks
|
||||
WHERE :start_height <= height",
|
||||
))?;
|
||||
let mut stmt_start_tree_size = conn.prepare_cached(&format!(
|
||||
"SELECT MAX({table_prefix}_commitment_tree_size - {output_count_col})
|
||||
FROM blocks
|
||||
WHERE height <= :start_height",
|
||||
))?;
|
||||
let mut stmt_end_tree_size_at = conn.prepare_cached(&format!(
|
||||
"SELECT {table_prefix}_commitment_tree_size
|
||||
FROM blocks
|
||||
WHERE height = :height",
|
||||
))?;
|
||||
|
||||
if fully_scanned_height == chain_tip_height {
|
||||
// Compute the total blocks scanned since the wallet birthday on either side of
|
||||
// the recover-until height.
|
||||
let recover = recover_until_height
|
||||
.map(|end_height| {
|
||||
stmt_scanned_count_between.query_row(
|
||||
named_params! {
|
||||
":start_height": u32::from(birthday_height),
|
||||
":end_height": u32::from(end_height),
|
||||
},
|
||||
|row| {
|
||||
let recovered = row.get::<_, Option<u64>>(0)?;
|
||||
Ok(recovered.map(|n| Ratio::new(n, n)))
|
||||
},
|
||||
)
|
||||
.optional()?
|
||||
.flatten()
|
||||
.map(Ok)
|
||||
.or_else(|| {
|
||||
conn.query_row(
|
||||
"SELECT MAX(sapling_commitment_tree_size - sapling_output_count)
|
||||
FROM blocks
|
||||
WHERE height <= :start_height",
|
||||
})
|
||||
.transpose()?
|
||||
// If none of the wallet's accounts have a recover-until height, then we can't
|
||||
// (yet) distinguish general scanning from recovery, so treat the wallet as
|
||||
// fully recovered.
|
||||
.unwrap_or_else(|| Some(Ratio::new(1, 1)));
|
||||
let scan = stmt_scanned_count_from.query_row(
|
||||
named_params! {
|
||||
":start_height": u32::from(
|
||||
recover_until_height.map(|h| h + 1)
|
||||
.unwrap_or(birthday_height)
|
||||
),
|
||||
},
|
||||
|row| {
|
||||
let scanned = row.get::<_, Option<u64>>(0)?;
|
||||
Ok(scanned.map(|n| Ratio::new(n, n)))
|
||||
},
|
||||
)?;
|
||||
Ok(Progress { scan, recover })
|
||||
} else {
|
||||
// Get the starting note commitment tree size from the wallet birthday, or failing that
|
||||
// from the blocks table.
|
||||
let start_size = conn
|
||||
.query_row(
|
||||
&format!(
|
||||
"SELECT birthday_{table_prefix}_tree_size
|
||||
FROM accounts
|
||||
WHERE birthday_height = :birthday_height",
|
||||
),
|
||||
named_params![":birthday_height": u32::from(birthday_height)],
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
)
|
||||
.optional()?
|
||||
.flatten()
|
||||
.map(Ok)
|
||||
.or_else(|| {
|
||||
stmt_start_tree_size
|
||||
.query_row(
|
||||
named_params![":start_height": u32::from(birthday_height)],
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
)
|
||||
.optional()
|
||||
.map(|opt| opt.flatten())
|
||||
.transpose()
|
||||
})
|
||||
.transpose()?;
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// Compute the total blocks scanned so far above the starting height
|
||||
let scanned_count = conn.query_row(
|
||||
"SELECT SUM(sapling_output_count)
|
||||
FROM blocks
|
||||
WHERE height > :start_height",
|
||||
named_params![":start_height": u32::from(birthday_height)],
|
||||
// Get the note commitment tree size as of the end of the recover-until height.
|
||||
let recover_until_size = recover_until_height
|
||||
.map(|end_height| {
|
||||
stmt_start_tree_size
|
||||
.query_row(
|
||||
named_params![":start_height": u32::from(end_height + 1)],
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
)
|
||||
.optional()
|
||||
.map(|opt| opt.flatten())
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// Count the total outputs scanned so far on the birthday side of the
|
||||
// recover-until height.
|
||||
let recovered_count = recover_until_height
|
||||
.map(|end_height| {
|
||||
stmt_scanned_count_between.query_row(
|
||||
named_params! {
|
||||
":start_height": u32::from(birthday_height),
|
||||
":end_height": u32::from(end_height),
|
||||
},
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// In case we didn't have information about the tree size at the recover-until
|
||||
// height, get the tree size from a nearby subtree. It's fine for this to be
|
||||
// approximate; it just shifts the boundary between scan and recover progress.
|
||||
let min_tree_size = conn
|
||||
.query_row(
|
||||
&format!(
|
||||
"SELECT MIN(shard_index)
|
||||
FROM {table_prefix}_tree_shards
|
||||
WHERE subtree_end_height > :start_height
|
||||
OR subtree_end_height IS NULL",
|
||||
),
|
||||
named_params! {
|
||||
":start_height": u32::from(recover_until_height.unwrap_or(birthday_height) + 1),
|
||||
},
|
||||
|row| {
|
||||
let min_tree_size = row
|
||||
.get::<_, Option<u64>>(0)?
|
||||
.map(|min_idx| min_idx << shard_height);
|
||||
Ok(min_tree_size)
|
||||
},
|
||||
)
|
||||
.optional()?
|
||||
.flatten();
|
||||
|
||||
// If we've scanned the block at the chain tip, we know how many notes are
|
||||
// currently in the tree.
|
||||
let tip_tree_size = match stmt_end_tree_size_at
|
||||
.query_row(
|
||||
named_params! {":height": u32::from(chain_tip_height)},
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
)
|
||||
.optional()?
|
||||
.flatten()
|
||||
{
|
||||
Some(tree_size) => Some(tree_size),
|
||||
None => {
|
||||
// Estimate the size of the tree by linear extrapolation from available
|
||||
// data closest to the chain tip.
|
||||
//
|
||||
// - If we have scanned blocks within the incomplete subtree, and we know
|
||||
// the tree size for the end of the most recent scanned range, then we
|
||||
// extrapolate from the start of the incomplete subtree:
|
||||
//
|
||||
// subtree
|
||||
// / \
|
||||
// / \
|
||||
// / \
|
||||
// / \
|
||||
// |<--------->| |
|
||||
// | scanned | tip
|
||||
// last_scanned
|
||||
//
|
||||
//
|
||||
// subtree
|
||||
// / \
|
||||
// / \
|
||||
// / \
|
||||
// / \
|
||||
// |<------->| |
|
||||
// | scanned | tip
|
||||
// last_scanned
|
||||
//
|
||||
// - If we don't have scanned blocks within the incomplete subtree, or we
|
||||
// don't know the tree size, then we extrapolate from the block-width of
|
||||
// the last complete subtree.
|
||||
//
|
||||
// This avoids having a sharp discontinuity in the progress percentages
|
||||
// shown to users, and gets more accurate the closer to the chain tip we
|
||||
// have scanned.
|
||||
//
|
||||
// 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.
|
||||
|
||||
// Get the tree size at the last scanned height, if known.
|
||||
let last_scanned = block_max_scanned(conn, params)?.and_then(|last_scanned| {
|
||||
match table_prefix {
|
||||
SAPLING_TABLES_PREFIX => last_scanned.sapling_tree_size(),
|
||||
#[cfg(feature = "orchard")]
|
||||
ORCHARD_TABLES_PREFIX => last_scanned.orchard_tree_size(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
.map(|tree_size| (last_scanned.block_height(), u64::from(tree_size)))
|
||||
});
|
||||
|
||||
// Get the last completed subtree.
|
||||
let last_completed_subtree = conn
|
||||
.query_row(
|
||||
&format!(
|
||||
"SELECT shard_index, subtree_end_height
|
||||
FROM {table_prefix}_tree_shards
|
||||
WHERE subtree_end_height IS NOT NULL
|
||||
ORDER BY shard_index DESC
|
||||
LIMIT 1"
|
||||
),
|
||||
[],
|
||||
|row| {
|
||||
Ok((
|
||||
incrementalmerkletree::Address::from_parts(
|
||||
incrementalmerkletree::Level::new(shard_height),
|
||||
row.get(0)?,
|
||||
),
|
||||
BlockHeight::from_u32(row.get(1)?),
|
||||
))
|
||||
},
|
||||
)
|
||||
// `None` if we have no subtree roots yet.
|
||||
.optional()?;
|
||||
|
||||
if let Some((last_completed_subtree, last_completed_subtree_end)) =
|
||||
last_completed_subtree
|
||||
{
|
||||
// If we know the tree size at the last scanned height, and that
|
||||
// height is within the incomplete subtree, extrapolate.
|
||||
let tip_tree_size =
|
||||
last_scanned.and_then(|(last_scanned, last_scanned_tree_size)| {
|
||||
(last_scanned > last_completed_subtree_end)
|
||||
.then(|| {
|
||||
let scanned_notes = last_scanned_tree_size
|
||||
- u64::from(last_completed_subtree.position_range_end());
|
||||
let scanned_range =
|
||||
u64::from(last_scanned - last_completed_subtree_end);
|
||||
let unscanned_range =
|
||||
u64::from(chain_tip_height - last_scanned);
|
||||
|
||||
(scanned_notes * unscanned_range)
|
||||
.checked_div(scanned_range)
|
||||
.map(|extrapolated_unscanned_notes| {
|
||||
last_scanned_tree_size + extrapolated_unscanned_notes
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
});
|
||||
|
||||
if let Some(tree_size) = tip_tree_size {
|
||||
Some(tree_size)
|
||||
} else if let Some(second_to_last_completed_subtree_end) =
|
||||
last_completed_subtree
|
||||
.index()
|
||||
.checked_sub(1)
|
||||
.and_then(|subtree_index| {
|
||||
conn.query_row(
|
||||
&format!(
|
||||
"SELECT subtree_end_height
|
||||
FROM {table_prefix}_tree_shards
|
||||
WHERE shard_index = :shard_index"
|
||||
),
|
||||
named_params! {":shard_index": subtree_index},
|
||||
|row| {
|
||||
Ok(row.get::<_, Option<_>>(0)?.map(BlockHeight::from_u32))
|
||||
},
|
||||
)
|
||||
.transpose()
|
||||
})
|
||||
.transpose()?
|
||||
{
|
||||
let notes_in_complete_subtrees =
|
||||
u64::from(last_completed_subtree.position_range_end());
|
||||
|
||||
let subtree_notes = 1 << shard_height;
|
||||
let subtree_range = u64::from(
|
||||
last_completed_subtree_end - second_to_last_completed_subtree_end,
|
||||
);
|
||||
let unscanned_range =
|
||||
u64::from(chain_tip_height - last_completed_subtree_end);
|
||||
|
||||
(subtree_notes * unscanned_range)
|
||||
.checked_div(subtree_range)
|
||||
.map(|extrapolated_incomplete_subtree_notes| {
|
||||
notes_in_complete_subtrees + extrapolated_incomplete_subtree_notes
|
||||
})
|
||||
} else {
|
||||
// There's only one completed subtree; its start height must
|
||||
// be the activation height for this shielded protocol.
|
||||
let subtree_notes = 1 << shard_height;
|
||||
|
||||
let subtree_range =
|
||||
u64::from(last_completed_subtree_end - pool_activation_height);
|
||||
let unscanned_range =
|
||||
u64::from(chain_tip_height - last_completed_subtree_end);
|
||||
|
||||
(subtree_notes * unscanned_range)
|
||||
.checked_div(subtree_range)
|
||||
.map(|extrapolated_incomplete_subtree_notes| {
|
||||
subtree_notes + extrapolated_incomplete_subtree_notes
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// We don't have subtree information, so give up. We'll get it soon.
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let recover = recovered_count
|
||||
.zip(recover_until_size)
|
||||
.map(|(recovered, end_size)| {
|
||||
start_size
|
||||
.or(min_tree_size)
|
||||
.zip(end_size)
|
||||
.map(|(start_size, end_size)| {
|
||||
Ratio::new(recovered.unwrap_or(0), end_size - start_size)
|
||||
})
|
||||
})
|
||||
// If none of the wallet's accounts have a recover-until height, then we can't
|
||||
// (yet) distinguish general scanning from recovery, so treat the wallet as
|
||||
// fully recovered.
|
||||
.unwrap_or_else(|| Some(Ratio::new(1, 1)));
|
||||
|
||||
let scan = if recover_until_height.map_or(false, |h| h == chain_tip_height) {
|
||||
// The wallet was likely just created for a recovery from seed, or with an
|
||||
// imported viewing key. In this state, it is fully synced as there is nothing
|
||||
// else for us to scan beyond `recover_until_height`; ensure we show 100%
|
||||
// instead of 0%.
|
||||
Some(Ratio::new(1, 1))
|
||||
} else {
|
||||
// Count the total outputs scanned so far on the chain tip side of the
|
||||
// recover-until height.
|
||||
let scanned_count = stmt_scanned_count_from.query_row(
|
||||
named_params![":start_height": u32::from(recover_until_height.unwrap_or(birthday_height) + 1)],
|
||||
|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 > :start_height
|
||||
OR subtree_end_height IS NULL",
|
||||
named_params![":start_height": u32::from(birthday_height)],
|
||||
|row| {
|
||||
let min_tree_size = row
|
||||
.get::<_, Option<u64>>(0)?
|
||||
.map(|min_idx| min_idx << SAPLING_SHARD_HEIGHT);
|
||||
let max_tree_size = row
|
||||
.get::<_, Option<u64>>(1)?
|
||||
.map(|max_idx| (max_idx + 1) << SAPLING_SHARD_HEIGHT);
|
||||
Ok(start_size.or(min_tree_size).zip(max_tree_size).map(
|
||||
|(min_tree_size, max_tree_size)| {
|
||||
Ratio::new(
|
||||
scanned_count.unwrap_or(0),
|
||||
max_tree_size - min_tree_size,
|
||||
)
|
||||
},
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()?
|
||||
.flatten())
|
||||
}
|
||||
recover_until_size
|
||||
.unwrap_or(start_size)
|
||||
.or(min_tree_size)
|
||||
.zip(tip_tree_size)
|
||||
.map(|(start_size, tip_tree_size)| {
|
||||
Ratio::new(scanned_count.unwrap_or(0), tip_tree_size - start_size)
|
||||
})
|
||||
};
|
||||
|
||||
Ok(Progress { scan, recover })
|
||||
}
|
||||
}
|
||||
|
||||
impl ScanProgress for SubtreeScanProgress {
|
||||
#[tracing::instrument(skip(conn, params))]
|
||||
fn sapling_scan_progress<P: consensus::Parameters>(
|
||||
&self,
|
||||
conn: &rusqlite::Connection,
|
||||
params: &P,
|
||||
birthday_height: BlockHeight,
|
||||
recover_until_height: Option<BlockHeight>,
|
||||
fully_scanned_height: BlockHeight,
|
||||
chain_tip_height: BlockHeight,
|
||||
) -> Result<Progress, SqliteClientError> {
|
||||
subtree_scan_progress(
|
||||
conn,
|
||||
params,
|
||||
SAPLING_TABLES_PREFIX,
|
||||
"sapling_output_count",
|
||||
SAPLING_SHARD_HEIGHT,
|
||||
params
|
||||
.activation_height(NetworkUpgrade::Sapling)
|
||||
.expect("Sapling activation height must be available."),
|
||||
birthday_height,
|
||||
recover_until_height,
|
||||
fully_scanned_height,
|
||||
chain_tip_height,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
#[tracing::instrument(skip(conn))]
|
||||
fn orchard_scan_progress(
|
||||
#[tracing::instrument(skip(conn, params))]
|
||||
fn orchard_scan_progress<P: consensus::Parameters>(
|
||||
&self,
|
||||
conn: &rusqlite::Connection,
|
||||
params: &P,
|
||||
birthday_height: BlockHeight,
|
||||
recover_until_height: Option<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(orchard_action_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 starting number of notes directly from the blocks table
|
||||
let start_size = conn
|
||||
.query_row(
|
||||
"SELECT birthday_orchard_tree_size
|
||||
FROM accounts
|
||||
WHERE birthday_height = :birthday_height",
|
||||
named_params![":birthday_height": u32::from(birthday_height)],
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
)
|
||||
.optional()?
|
||||
.flatten()
|
||||
.map(Ok)
|
||||
.or_else(|| {
|
||||
conn.query_row(
|
||||
"SELECT MAX(orchard_commitment_tree_size - orchard_action_count)
|
||||
FROM blocks
|
||||
WHERE height <= :start_height",
|
||||
named_params![":start_height": u32::from(birthday_height)],
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
)
|
||||
.optional()
|
||||
.map(|opt| opt.flatten())
|
||||
.transpose()
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// Compute the total blocks scanned so far above the starting height
|
||||
let scanned_count = conn.query_row(
|
||||
"SELECT SUM(orchard_action_count)
|
||||
FROM blocks
|
||||
WHERE height > :start_height",
|
||||
named_params![":start_height": u32::from(birthday_height)],
|
||||
|row| row.get::<_, Option<u64>>(0),
|
||||
)?;
|
||||
|
||||
// We don't have complete information on how many actions 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 orchard_tree_shards
|
||||
WHERE subtree_end_height > :start_height
|
||||
OR subtree_end_height IS NULL",
|
||||
named_params![":start_height": u32::from(birthday_height)],
|
||||
|row| {
|
||||
let min_tree_size = row
|
||||
.get::<_, Option<u64>>(0)?
|
||||
.map(|min_idx| min_idx << ORCHARD_SHARD_HEIGHT);
|
||||
let max_tree_size = row
|
||||
.get::<_, Option<u64>>(1)?
|
||||
.map(|max_idx| (max_idx + 1) << ORCHARD_SHARD_HEIGHT);
|
||||
Ok(start_size.or(min_tree_size).zip(max_tree_size).map(
|
||||
|(min_tree_size, max_tree_size)| {
|
||||
Ratio::new(
|
||||
scanned_count.unwrap_or(0),
|
||||
max_tree_size - min_tree_size,
|
||||
)
|
||||
},
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()?
|
||||
.flatten())
|
||||
}
|
||||
) -> Result<Progress, SqliteClientError> {
|
||||
subtree_scan_progress(
|
||||
conn,
|
||||
params,
|
||||
ORCHARD_TABLES_PREFIX,
|
||||
"orchard_action_count",
|
||||
ORCHARD_SHARD_HEIGHT,
|
||||
params
|
||||
.activation_height(NetworkUpgrade::Nu5)
|
||||
.expect("NU5 activation height must be available."),
|
||||
birthday_height,
|
||||
recover_until_height,
|
||||
fully_scanned_height,
|
||||
chain_tip_height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1041,39 +1277,59 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
|||
|
||||
let birthday_height =
|
||||
wallet_birthday(tx)?.expect("If a scan range exists, we know the wallet birthday.");
|
||||
let recover_until_height = recover_until_height(tx)?;
|
||||
|
||||
let fully_scanned_height =
|
||||
block_fully_scanned(tx, params)?.map_or(birthday_height - 1, |m| m.block_height());
|
||||
let summary_height = (chain_tip_height + 1).saturating_sub(std::cmp::max(min_confirmations, 1));
|
||||
|
||||
let sapling_scan_progress = progress.sapling_scan_progress(
|
||||
let sapling_progress = progress.sapling_scan_progress(
|
||||
tx,
|
||||
params,
|
||||
birthday_height,
|
||||
recover_until_height,
|
||||
fully_scanned_height,
|
||||
chain_tip_height,
|
||||
)?;
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_scan_progress = progress.orchard_scan_progress(
|
||||
let orchard_progress = progress.orchard_scan_progress(
|
||||
tx,
|
||||
params,
|
||||
birthday_height,
|
||||
recover_until_height,
|
||||
fully_scanned_height,
|
||||
chain_tip_height,
|
||||
)?;
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let orchard_scan_progress: Option<Ratio<u64>> = None;
|
||||
let orchard_progress: Progress = Progress {
|
||||
scan: None,
|
||||
recover: None,
|
||||
};
|
||||
|
||||
// Treat Sapling and Orchard outputs as having the same cost to scan.
|
||||
let scan_progress = sapling_scan_progress
|
||||
.zip(orchard_scan_progress)
|
||||
let scan_progress = sapling_progress
|
||||
.scan
|
||||
.zip(orchard_progress.scan)
|
||||
.map(|(s, o)| {
|
||||
Ratio::new(
|
||||
s.numerator() + o.numerator(),
|
||||
s.denominator() + o.denominator(),
|
||||
)
|
||||
})
|
||||
.or(sapling_scan_progress)
|
||||
.or(orchard_scan_progress);
|
||||
.or(sapling_progress.scan)
|
||||
.or(orchard_progress.scan);
|
||||
let recover_progress = sapling_progress
|
||||
.recover
|
||||
.zip(orchard_progress.recover)
|
||||
.map(|(s, o)| {
|
||||
Ratio::new(
|
||||
s.numerator() + o.numerator(),
|
||||
s.denominator() + o.denominator(),
|
||||
)
|
||||
})
|
||||
.or(sapling_progress.recover)
|
||||
.or(orchard_progress.recover);
|
||||
|
||||
let mut stmt_accounts = tx.prepare_cached("SELECT id FROM accounts")?;
|
||||
let mut account_balances = stmt_accounts
|
||||
|
@ -1296,6 +1552,7 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
|||
chain_tip_height,
|
||||
fully_scanned_height,
|
||||
scan_progress,
|
||||
recover_progress,
|
||||
next_sapling_subtree_index,
|
||||
#[cfg(feature = "orchard")]
|
||||
next_orchard_subtree_index,
|
||||
|
@ -1517,6 +1774,20 @@ pub(crate) fn account_birthday(
|
|||
.and_then(|opt| opt.ok_or(SqliteClientError::AccountUnknown))
|
||||
}
|
||||
|
||||
/// Returns the maximum recover-until height for accounts in the wallet.
|
||||
pub(crate) fn recover_until_height(
|
||||
conn: &rusqlite::Connection,
|
||||
) -> Result<Option<BlockHeight>, rusqlite::Error> {
|
||||
conn.query_row(
|
||||
"SELECT MAX(recover_until_height) FROM accounts",
|
||||
[],
|
||||
|row| {
|
||||
row.get::<_, Option<u32>>(0)
|
||||
.map(|opt| opt.map(BlockHeight::from))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the minimum and maximum heights for blocks stored in the wallet database.
|
||||
pub(crate) fn block_height_extrema(
|
||||
conn: &rusqlite::Connection,
|
||||
|
|
|
@ -570,7 +570,7 @@ pub(crate) mod tests {
|
|||
pool::ShieldedPoolTester, sapling::SaplingPoolTester, AddressType, FakeCompactOutput,
|
||||
InitialChainState, TestBuilder, TestState,
|
||||
},
|
||||
AccountBirthday, Ratio, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT,
|
||||
AccountBirthday, Ratio, WalletRead, WalletWrite,
|
||||
};
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
|
@ -1292,8 +1292,21 @@ pub(crate) mod tests {
|
|||
let birthday = account.birthday();
|
||||
let sap_active = st.sapling_activation_height();
|
||||
|
||||
// The account is configured without a recover-until height, so is by definition
|
||||
// fully recovered, and we count 1 per pool for both numerator and denominator.
|
||||
let fully_recovered = {
|
||||
let n = 1;
|
||||
#[cfg(feature = "orchard")]
|
||||
let n = n * 2;
|
||||
Some(Ratio::new(n, n))
|
||||
};
|
||||
|
||||
// We have scan ranges and a subtree, but have scanned no blocks.
|
||||
let summary = st.get_wallet_summary(1);
|
||||
assert_eq!(
|
||||
summary.as_ref().and_then(|s| s.recovery_progress()),
|
||||
fully_recovered,
|
||||
);
|
||||
assert_eq!(summary.and_then(|s| s.scan_progress()), None);
|
||||
|
||||
// Set up prior chain state. This simulates us having imported a wallet
|
||||
|
@ -1332,24 +1345,29 @@ pub(crate) mod tests {
|
|||
let summary = st.get_wallet_summary(1);
|
||||
assert_eq!(summary.as_ref().map(|s| T::next_subtree_index(s)), Some(0));
|
||||
|
||||
assert_eq!(
|
||||
summary.as_ref().and_then(|s| s.recovery_progress()),
|
||||
fully_recovered,
|
||||
);
|
||||
|
||||
// Progress denominator depends on which pools are enabled (which changes the
|
||||
// initial tree states). Here we compute the denominator based upon the fact that
|
||||
// the trees are the same size at present.
|
||||
let expected_denom = (1 << SAPLING_SHARD_HEIGHT) * 2 - frontier_tree_size;
|
||||
// initial tree states), and is extrapolated from the scanned range.
|
||||
let expected_denom = 10
|
||||
+ ((1234 + 10) * (prior_tip - max_scanned)) / (max_scanned - (birthday.height() - 10));
|
||||
#[cfg(feature = "orchard")]
|
||||
let expected_denom = expected_denom * 2;
|
||||
let expected_denom = expected_denom + 1;
|
||||
assert_eq!(
|
||||
summary.and_then(|s| s.scan_progress()),
|
||||
Some(Ratio::new(1, u64::from(expected_denom)))
|
||||
);
|
||||
|
||||
// Now simulate shutting down, and then restarting 70 blocks later, after a shard
|
||||
// has been completed in one pool. This shard will have index 2, as our birthday
|
||||
// was in shard 1.
|
||||
// Now simulate shutting down, and then restarting 70 blocks later, after the
|
||||
// shard containing our birthday has been completed in one pool.
|
||||
let last_shard_start = prior_tip + 50;
|
||||
T::put_subtree_roots(
|
||||
&mut st,
|
||||
2,
|
||||
1,
|
||||
&[CommitmentTreeRoot::from_parts(
|
||||
last_shard_start,
|
||||
// fake a hash, the value doesn't matter
|
||||
|
@ -1365,13 +1383,17 @@ pub(crate) mod tests {
|
|||
.conn
|
||||
.prepare("SELECT shard_index, subtree_end_height FROM sapling_tree_shards")
|
||||
.unwrap();
|
||||
(shard_stmt
|
||||
.query_and_then::<_, rusqlite::Error, _, _>([], |row| {
|
||||
Ok((row.get::<_, u32>(0)?, row.get::<_, Option<u32>>(1)?))
|
||||
})
|
||||
assert_eq!(
|
||||
(shard_stmt
|
||||
.query_and_then::<_, rusqlite::Error, _, _>([], |row| {
|
||||
Ok((row.get::<_, u32>(0)?, row.get::<_, Option<u32>>(1)?))
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<_>, _>>())
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<_>, _>>())
|
||||
.unwrap();
|
||||
.len(),
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -1381,13 +1403,21 @@ pub(crate) mod tests {
|
|||
.conn
|
||||
.prepare("SELECT shard_index, subtree_end_height FROM orchard_tree_shards")
|
||||
.unwrap();
|
||||
(shard_stmt
|
||||
.query_and_then::<_, rusqlite::Error, _, _>([], |row| {
|
||||
Ok((row.get::<_, u32>(0)?, row.get::<_, Option<u32>>(1)?))
|
||||
})
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let expected_shards = 0;
|
||||
#[cfg(feature = "orchard")]
|
||||
let expected_shards = 2;
|
||||
assert_eq!(
|
||||
(shard_stmt
|
||||
.query_and_then::<_, rusqlite::Error, _, _>([], |row| {
|
||||
Ok((row.get::<_, u32>(0)?, row.get::<_, Option<u32>>(1)?))
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<_>, _>>())
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<_>, _>>())
|
||||
.unwrap();
|
||||
.len(),
|
||||
expected_shards,
|
||||
);
|
||||
}
|
||||
|
||||
let new_tip = last_shard_start + 20;
|
||||
|
@ -1409,10 +1439,16 @@ pub(crate) mod tests {
|
|||
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
// We've crossed a subtree boundary, but only in one pool. We still only have one scanned
|
||||
// note but in the pool where we crossed the subtree boundary we have two shards worth of
|
||||
// notes to scan.
|
||||
let expected_denom = expected_denom + (1 << 16);
|
||||
// We've crossed a subtree boundary, but only in one pool.
|
||||
let expected_denom = (1 << 16) * 2
|
||||
+ ((1 << 16) * (new_tip - last_shard_start))
|
||||
/ (last_shard_start - (birthday.height() - 10))
|
||||
- frontier_tree_size;
|
||||
#[cfg(feature = "orchard")]
|
||||
let expected_denom = expected_denom
|
||||
+ (10
|
||||
+ ((1234 + 10) * (new_tip - max_scanned))
|
||||
/ (max_scanned - (birthday.height() - 10)));
|
||||
let summary = st.get_wallet_summary(1);
|
||||
assert_eq!(
|
||||
summary.and_then(|s| s.scan_progress()),
|
||||
|
|
Loading…
Reference in New Issue