Merge pull request #1541 from zcash/1070-zcb-wallet-summary-progress

Improve progress representation in `WalletSummary`
This commit is contained in:
Kris Nuttycombe 2024-09-20 12:13:01 -06:00 committed by GitHub
commit 8b5236a45a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 573 additions and 207 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -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")]

View File

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

View File

@ -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,

View File

@ -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()),