Skip to content

Commit d872202

Browse files
committed
zcash_client_sqlite: Improve estimate for tree size at chain tip
After splitting recover progress out from scan progress, the overestimation of the chain tip tree size became much more noticeable. We now extrapolate from the most recent known "notes per block" rate.
1 parent 52f0cf0 commit d872202

File tree

3 files changed

+217
-29
lines changed

3 files changed

+217
-29
lines changed

zcash_client_sqlite/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ use zip32::fingerprint::SeedFingerprint;
7474

7575
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
7676

77-
#[cfg(any(feature = "test-dependencies", not(feature = "orchard")))]
77+
#[cfg(any(test, feature = "test-dependencies", not(feature = "orchard")))]
7878
use zcash_protocol::PoolType;
7979

8080
#[cfg(feature = "orchard")]

zcash_client_sqlite/src/wallet.rs

+207-22
Original file line numberDiff line numberDiff line change
@@ -815,19 +815,21 @@ pub(crate) struct Progress {
815815
}
816816

817817
pub(crate) trait ScanProgress {
818-
fn sapling_scan_progress(
818+
fn sapling_scan_progress<P: consensus::Parameters>(
819819
&self,
820820
conn: &rusqlite::Connection,
821+
params: &P,
821822
birthday_height: BlockHeight,
822823
recover_until_height: Option<BlockHeight>,
823824
fully_scanned_height: BlockHeight,
824825
chain_tip_height: BlockHeight,
825826
) -> Result<Progress, SqliteClientError>;
826827

827828
#[cfg(feature = "orchard")]
828-
fn orchard_scan_progress(
829+
fn orchard_scan_progress<P: consensus::Parameters>(
829830
&self,
830831
conn: &rusqlite::Connection,
832+
params: &P,
831833
birthday_height: BlockHeight,
832834
recover_until_height: Option<BlockHeight>,
833835
fully_scanned_height: BlockHeight,
@@ -844,6 +846,7 @@ fn subtree_scan_progress(
844846
table_prefix: &'static str,
845847
output_count_col: &'static str,
846848
shard_height: u8,
849+
pool_activation_height: BlockHeight,
847850
birthday_height: BlockHeight,
848851
recover_until_height: Option<BlockHeight>,
849852
fully_scanned_height: BlockHeight,
@@ -864,6 +867,11 @@ fn subtree_scan_progress(
864867
FROM blocks
865868
WHERE height <= :start_height",
866869
))?;
870+
let mut stmt_end_tree_size_at = conn.prepare_cached(&format!(
871+
"SELECT {table_prefix}_commitment_tree_size
872+
FROM blocks
873+
WHERE height = :height",
874+
))?;
867875

868876
if fully_scanned_height == chain_tip_height {
869877
// Compute the total blocks scanned since the wallet birthday on either side of
@@ -956,16 +964,13 @@ fn subtree_scan_progress(
956964
})
957965
.transpose()?;
958966

959-
// We don't have complete information on how many outputs will exist in the shard at
960-
// the chain tip without having scanned the chain tip block, so we overestimate by
961-
// computing the maximum possible number of notes directly from the shard indices.
962-
//
963-
// TODO: it would be nice to be able to reliably have the size of the commitment tree
964-
// at the chain tip without having to have scanned that block.
965-
let (min_tree_size, max_tree_size) = conn
967+
// In case we didn't have information about the tree size at the recover-until
968+
// height, get the tree size from a nearby subtree. It's fine for this to be
969+
// approximate; it just shifts the boundary between scan and recover progress.
970+
let min_tree_size = conn
966971
.query_row(
967972
&format!(
968-
"SELECT MIN(shard_index), MAX(shard_index)
973+
"SELECT MIN(shard_index)
969974
FROM {table_prefix}_tree_shards
970975
WHERE subtree_end_height > :start_height
971976
OR subtree_end_height IS NULL",
@@ -975,13 +980,183 @@ fn subtree_scan_progress(
975980
let min_tree_size = row
976981
.get::<_, Option<u64>>(0)?
977982
.map(|min_idx| min_idx << shard_height);
978-
let max_tree_size = row
979-
.get::<_, Option<u64>>(1)?
980-
.map(|max_idx| (max_idx + 1) << shard_height);
981-
Ok(min_tree_size.zip(max_tree_size))
983+
Ok(min_tree_size)
982984
},
983985
)
984-
.optional()?.flatten().unzip();
986+
.optional()?.flatten();
987+
988+
// If we've scanned the block at the chain tip, we know how many notes are
989+
// currently in the tree.
990+
let tip_tree_size = match stmt_end_tree_size_at
991+
.query_row(
992+
named_params! {":height": u32::from(chain_tip_height)},
993+
|row| row.get::<_, Option<u64>>(0),
994+
)
995+
.optional()?
996+
.flatten()
997+
{
998+
Some(tree_size) => Some(tree_size),
999+
None => {
1000+
// Estimate the size of the tree by linear extrapolation from available
1001+
// data closest to the chain tip.
1002+
//
1003+
// - If we have scanned blocks within the incomplete subtree, and we know
1004+
// the tree size for the end of the most recent scanned range, then we
1005+
// extrapolate from the start of the incomplete subtree:
1006+
//
1007+
// subtree
1008+
// / \
1009+
// / \
1010+
// / \
1011+
// / \
1012+
// |<--------->| |
1013+
// | scanned | tip
1014+
// last_scanned
1015+
//
1016+
//
1017+
// subtree
1018+
// / \
1019+
// / \
1020+
// / \
1021+
// / \
1022+
// |<------->| |
1023+
// | scanned | tip
1024+
// last_scanned
1025+
//
1026+
// - If we don't have scanned blocks within the incomplete subtree, or we
1027+
// don't know the tree size, then we extrapolate from the block-width of
1028+
// the last complete subtree.
1029+
//
1030+
// This avoids having a sharp discontinuity in the progress percentages
1031+
// shown to users, and gets more accurate the closer to the chain tip we
1032+
// have scanned.
1033+
//
1034+
// TODO: it would be nice to be able to reliably have the size of the
1035+
// commitment tree at the chain tip without having to have scanned that
1036+
// block.
1037+
1038+
// Get the last scanned height.
1039+
let last_scanned = conn
1040+
.query_row(
1041+
"SELECT block_range_end
1042+
FROM scan_queue
1043+
WHERE priority = :priority
1044+
ORDER BY block_range_start DESC
1045+
LIMIT 1",
1046+
named_params![":priority": priority_code(&ScanPriority::Scanned)],
1047+
|row| {
1048+
// Scan ranges are end-exclusive.
1049+
Ok(BlockHeight::from_u32(row.get(0)?) - 1)
1050+
},
1051+
)
1052+
// `None` if we haven't scanned anything.
1053+
.optional()?;
1054+
1055+
// Get the tree size at the last scanned height, if known.
1056+
let last_scanned_tree_size = last_scanned
1057+
.map(|last_scanned| {
1058+
stmt_end_tree_size_at
1059+
.query_row(named_params! {":height": u32::from(last_scanned)}, |row| {
1060+
row.get::<_, Option<u64>>(0)
1061+
})
1062+
})
1063+
.transpose()?
1064+
.flatten();
1065+
1066+
// Get the last completed subtree.
1067+
let last_completed_subtree = conn
1068+
.query_row(
1069+
&format!(
1070+
"SELECT shard_index, subtree_end_height
1071+
FROM {table_prefix}_tree_shards
1072+
WHERE subtree_end_height IS NOT NULL
1073+
ORDER BY shard_index DESC
1074+
LIMIT 1"
1075+
),
1076+
[],
1077+
|row| Ok((row.get::<_, u64>(0)?, BlockHeight::from_u32(row.get(1)?))),
1078+
)
1079+
// `None` if we have no subtree roots yet.
1080+
.optional()?;
1081+
1082+
if let Some((last_completed_subtree_index, last_completed_subtree_height)) =
1083+
last_completed_subtree
1084+
{
1085+
// If we know the tree size at the last scanned height, extrapolate.
1086+
let tip_tree_size = last_scanned.zip(last_scanned_tree_size).and_then(
1087+
|(last_scanned, last_scanned_tree_size)| {
1088+
let scanned_notes = last_scanned_tree_size
1089+
- (last_completed_subtree_index << shard_height);
1090+
let scanned_range =
1091+
u64::from(last_scanned - last_completed_subtree_height);
1092+
let unscanned_range = u64::from(chain_tip_height - last_scanned);
1093+
1094+
(scanned_notes * unscanned_range)
1095+
.checked_div(scanned_range)
1096+
.map(|extrapolated_unscanned_notes| {
1097+
last_scanned_tree_size + extrapolated_unscanned_notes
1098+
})
1099+
},
1100+
);
1101+
1102+
if let Some(tree_size) = tip_tree_size {
1103+
Some(tree_size)
1104+
} else if let Some(second_to_last_completed_subtree_height) =
1105+
last_completed_subtree_index
1106+
.checked_sub(1)
1107+
.and_then(|subtree_index| {
1108+
conn.query_row(
1109+
&format!(
1110+
"SELECT subtree_end_height
1111+
FROM {table_prefix}_tree_shards
1112+
WHERE shard_index = :shard_index"
1113+
),
1114+
named_params! {":shard_index": subtree_index},
1115+
|row| {
1116+
Ok(row.get::<_, Option<_>>(0)?.map(BlockHeight::from_u32))
1117+
},
1118+
)
1119+
.transpose()
1120+
})
1121+
.transpose()?
1122+
{
1123+
let notes_in_complete_subtrees =
1124+
(last_completed_subtree_index + 1) << shard_height;
1125+
1126+
let subtree_notes = 1 << shard_height;
1127+
let subtree_range = u64::from(
1128+
last_completed_subtree_height - second_to_last_completed_subtree_height,
1129+
);
1130+
let unscanned_range =
1131+
u64::from(chain_tip_height - last_completed_subtree_height);
1132+
1133+
(subtree_notes * unscanned_range)
1134+
.checked_div(subtree_range)
1135+
.map(|extrapolated_incomplete_subtree_notes| {
1136+
notes_in_complete_subtrees + extrapolated_incomplete_subtree_notes
1137+
})
1138+
} else {
1139+
// There's only one completed subtree; its start height must
1140+
// be the activation height for this shielded protocol.
1141+
let scanned_notes = last_completed_subtree_index << shard_height;
1142+
1143+
let subtree_range =
1144+
u64::from(last_completed_subtree_height - pool_activation_height);
1145+
let unscanned_range =
1146+
u64::from(chain_tip_height - last_completed_subtree_height);
1147+
1148+
(scanned_notes * unscanned_range)
1149+
.checked_div(subtree_range)
1150+
.map(|extrapolated_unscanned_notes| {
1151+
scanned_notes + extrapolated_unscanned_notes
1152+
})
1153+
}
1154+
} else {
1155+
// We don't have subtree information, so give up. We'll get it soon.
1156+
None
1157+
}
1158+
}
1159+
};
9851160

9861161
let recover = recovered_count
9871162
.zip(recover_until_size)
@@ -1018,9 +1193,9 @@ fn subtree_scan_progress(
10181193
recover_until_size
10191194
.unwrap_or(start_size)
10201195
.or(min_tree_size)
1021-
.zip(max_tree_size)
1022-
.map(|(min_tree_size, max_tree_size)| {
1023-
Ratio::new(scanned_count.unwrap_or(0), max_tree_size - min_tree_size)
1196+
.zip(tip_tree_size)
1197+
.map(|(start_size, tip_tree_size)| {
1198+
Ratio::new(scanned_count.unwrap_or(0), tip_tree_size - start_size)
10241199
})
10251200
};
10261201

@@ -1029,10 +1204,11 @@ fn subtree_scan_progress(
10291204
}
10301205

10311206
impl ScanProgress for SubtreeScanProgress {
1032-
#[tracing::instrument(skip(conn))]
1033-
fn sapling_scan_progress(
1207+
#[tracing::instrument(skip(conn, params))]
1208+
fn sapling_scan_progress<P: consensus::Parameters>(
10341209
&self,
10351210
conn: &rusqlite::Connection,
1211+
params: &P,
10361212
birthday_height: BlockHeight,
10371213
recover_until_height: Option<BlockHeight>,
10381214
fully_scanned_height: BlockHeight,
@@ -1043,6 +1219,9 @@ impl ScanProgress for SubtreeScanProgress {
10431219
SAPLING_TABLES_PREFIX,
10441220
"sapling_output_count",
10451221
SAPLING_SHARD_HEIGHT,
1222+
params
1223+
.activation_height(NetworkUpgrade::Sapling)
1224+
.expect("Sapling activation height must be available."),
10461225
birthday_height,
10471226
recover_until_height,
10481227
fully_scanned_height,
@@ -1051,10 +1230,11 @@ impl ScanProgress for SubtreeScanProgress {
10511230
}
10521231

10531232
#[cfg(feature = "orchard")]
1054-
#[tracing::instrument(skip(conn))]
1055-
fn orchard_scan_progress(
1233+
#[tracing::instrument(skip(conn, params))]
1234+
fn orchard_scan_progress<P: consensus::Parameters>(
10561235
&self,
10571236
conn: &rusqlite::Connection,
1237+
params: &P,
10581238
birthday_height: BlockHeight,
10591239
recover_until_height: Option<BlockHeight>,
10601240
fully_scanned_height: BlockHeight,
@@ -1065,6 +1245,9 @@ impl ScanProgress for SubtreeScanProgress {
10651245
ORCHARD_TABLES_PREFIX,
10661246
"orchard_action_count",
10671247
ORCHARD_SHARD_HEIGHT,
1248+
params
1249+
.activation_height(NetworkUpgrade::Nu5)
1250+
.expect("NU5 activation height must be available."),
10681251
birthday_height,
10691252
recover_until_height,
10701253
fully_scanned_height,
@@ -1104,6 +1287,7 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
11041287

11051288
let sapling_progress = progress.sapling_scan_progress(
11061289
tx,
1290+
params,
11071291
birthday_height,
11081292
recover_until_height,
11091293
fully_scanned_height,
@@ -1113,6 +1297,7 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
11131297
#[cfg(feature = "orchard")]
11141298
let orchard_progress = progress.orchard_scan_progress(
11151299
tx,
1300+
params,
11161301
birthday_height,
11171302
recover_until_height,
11181303
fully_scanned_height,

zcash_client_sqlite/src/wallet/scanning.rs

+9-6
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ pub(crate) mod tests {
570570
pool::ShieldedPoolTester, sapling::SaplingPoolTester, AddressType, FakeCompactOutput,
571571
InitialChainState, TestBuilder, TestState,
572572
},
573-
AccountBirthday, Ratio, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT,
573+
AccountBirthday, Ratio, WalletRead, WalletWrite,
574574
};
575575
use zcash_primitives::{
576576
block::BlockHash,
@@ -1351,14 +1351,14 @@ pub(crate) mod tests {
13511351
);
13521352

13531353
// Progress denominator depends on which pools are enabled (which changes the
1354-
// initial tree states). Here we compute the denominator based upon the fact that
1355-
// the trees are the same size at present.
1356-
let expected_denom = (1 << SAPLING_SHARD_HEIGHT) * 2 - frontier_tree_size;
1354+
// initial tree states), and is extrapolated from the scanned range.
1355+
let expected_denom: u64 = 2628;
13571356
#[cfg(feature = "orchard")]
13581357
let expected_denom = expected_denom * 2;
1358+
let expected_denom = expected_denom + 1;
13591359
assert_eq!(
13601360
summary.and_then(|s| s.scan_progress()),
1361-
Some(Ratio::new(1, u64::from(expected_denom)))
1361+
Some(Ratio::new(1, expected_denom))
13621362
);
13631363

13641364
// Now simulate shutting down, and then restarting 70 blocks later, after a shard
@@ -1430,11 +1430,14 @@ pub(crate) mod tests {
14301430
// We've crossed a subtree boundary, but only in one pool. We still only have one scanned
14311431
// note but in the pool where we crossed the subtree boundary we have two shards worth of
14321432
// notes to scan.
1433+
let expected_denom: u64 = 2823;
1434+
#[cfg(feature = "orchard")]
1435+
let expected_denom = expected_denom + 11_794;
14331436
let expected_denom = expected_denom + (1 << 16);
14341437
let summary = st.get_wallet_summary(1);
14351438
assert_eq!(
14361439
summary.and_then(|s| s.scan_progress()),
1437-
Some(Ratio::new(1, u64::from(expected_denom)))
1440+
Some(Ratio::new(1, expected_denom))
14381441
);
14391442
}
14401443

0 commit comments

Comments
 (0)