use zcash_client_backend::data_api::scanning::{ScanPriority, ScanRange};
use zcash_protocol::consensus::{self, BlockHeight};
use crate::error::SqliteClientError;
use super::super::scanning;
use ScanPriority::*;
fn latest_pool_activation<P: consensus::Parameters>(params: &P) -> Option<BlockHeight> {
#[cfg(feature = "orchard")]
{
params.activation_height(consensus::NetworkUpgrade::Nu6_3)
}
#[cfg(not(feature = "orchard"))]
{
let _ = params;
None
}
}
pub(crate) fn suggest_scan_ranges<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
) -> Result<Vec<ScanRange>, SqliteClientError> {
let ranges = scanning::suggest_scan_ranges(conn, Historic)?;
Ok(derive_latest_pool_activation(
ranges,
latest_pool_activation(params),
))
}
fn derive_latest_pool_activation(
ranges: Vec<ScanRange>,
activation: Option<BlockHeight>,
) -> Vec<ScanRange> {
let Some(activation) = activation else {
return ranges;
};
if !ranges
.iter()
.any(|r| r.priority() == Historic && r.block_range().start < activation)
{
return ranges;
}
let promote =
|r: ScanRange| ScanRange::from_parts(r.block_range().clone(), LatestPoolActivation);
let mut out = Vec::with_capacity(ranges.len() + 1);
for range in ranges {
if range.priority() != Historic {
out.push(range);
continue;
}
match (
range.truncate_end(activation),
range.truncate_start(activation),
) {
(Some(below), Some(above)) => {
out.push(below);
out.push(promote(above));
}
(None, Some(above)) => out.push(promote(above)),
_ => out.push(range),
}
}
out.sort_by(|a, b| {
b.priority()
.cmp(&a.priority())
.then_with(|| b.block_range().end.cmp(&a.block_range().end))
});
out
}
#[cfg(test)]
mod tests {
use nonempty::NonEmpty;
use rusqlite::Connection;
use zcash_client_backend::data_api::{
WalletRead, WalletWrite,
scanning::{ScanPriority, ScanRange, spanning_tree::testing::scan_range},
testing::{TestBuilder, TestState},
};
use zcash_primitives::block::BlockHash;
use zcash_protocol::{consensus::BlockHeight, local_consensus::LocalNetwork};
use crate::{
error::SqliteClientError,
testing::db::{TestDb, TestDbFactory},
wallet::scanning::{self, insert_queue_entries, parse_priority_code, priority_code},
};
use super::derive_latest_pool_activation;
use ScanPriority::*;
const NU6_3_ACTIVATION: u32 = 300_000;
const PRIOR_NU6_ACTIVATION: u32 = 200_000;
fn nu6_3_active_network() -> LocalNetwork {
let prior = Some(BlockHeight::from_u32(PRIOR_NU6_ACTIVATION));
LocalNetwork {
nu6: prior,
nu6_1: prior,
nu6_2: prior,
nu6_3: Some(BlockHeight::from_u32(NU6_3_ACTIVATION)),
..TestBuilder::<(), ()>::DEFAULT_NETWORK
}
}
fn pre_nu6_3_birthday_wallet() -> TestState<(), TestDb, LocalNetwork> {
TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_network(nu6_3_active_network())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build()
}
#[test]
fn priority_code_round_trips_in_enum_order() {
let all = [
Ignored,
Scanned,
Historic,
LatestPoolActivation,
OpenAdjacent,
FoundNote,
ChainTip,
Verify,
];
assert_eq!(
all.iter().map(priority_code).collect::<Vec<_>>(),
vec![0, 10, 20, 25, 30, 40, 50, 60],
);
for priority in all {
assert_eq!(
parse_priority_code(priority_code(&priority)),
Some(priority)
);
}
for pair in all.windows(2) {
assert!(
pair[0] < pair[1],
"{:?} must order below {:?}",
pair[0],
pair[1],
);
}
}
#[test]
fn latest_pool_activation_orders_between_historic_and_open_adjacent() {
assert!(Historic < LatestPoolActivation);
assert!(LatestPoolActivation < OpenAdjacent);
assert!(LatestPoolActivation < FoundNote);
assert!(LatestPoolActivation < ChainTip);
}
fn suggest(
conn: &Connection,
min_priority: ScanPriority,
activation: Option<BlockHeight>,
) -> Result<Vec<ScanRange>, SqliteClientError> {
scanning::suggest_scan_ranges(conn, min_priority)
.map(|ranges| derive_latest_pool_activation(ranges, activation))
}
fn suggested(entries: &[ScanRange], activation: Option<u32>) -> Vec<(ScanPriority, u32, u32)> {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(crate::wallet::db::TABLE_SCAN_QUEUE)
.unwrap();
insert_queue_entries(&conn, entries.iter()).unwrap();
suggest(&conn, Ignored, activation.map(BlockHeight::from_u32))
.unwrap()
.into_iter()
.map(|r| {
(
r.priority(),
u32::from(r.block_range().start),
u32::from(r.block_range().end),
)
})
.collect()
}
#[test]
fn suggest_scan_ranges_prioritizes_post_activation_history() {
assert_eq!(
suggested(
&[scan_range(100_000..400_001, ScanPriority::Historic)],
Some(NU6_3_ACTIVATION),
),
vec![
(LatestPoolActivation, NU6_3_ACTIVATION, 400_001),
(Historic, 100_000, NU6_3_ACTIVATION),
],
);
}
#[test]
fn suggest_scan_ranges_relabels_only_historic_coverage() {
assert_eq!(
suggested(
&[
scan_range(100_000..310_000, ScanPriority::Historic), scan_range(310_000..340_000, ScanPriority::Ignored), scan_range(340_000..360_000, ScanPriority::Scanned), scan_range(360_000..380_000, ScanPriority::FoundNote), scan_range(380_000..400_001, ScanPriority::Historic), ],
Some(NU6_3_ACTIVATION),
),
vec![
(FoundNote, 360_000, 380_000),
(LatestPoolActivation, 380_000, 400_001),
(LatestPoolActivation, NU6_3_ACTIVATION, 310_000),
(Historic, 100_000, NU6_3_ACTIVATION),
(Scanned, 340_000, 360_000),
(Ignored, 310_000, 340_000),
],
);
}
#[test]
fn verify_still_precedes_latest_pool_activation() {
let suggestions = suggested(
&[
scan_range(100_000..350_000, ScanPriority::Historic),
scan_range(350_000..350_010, ScanPriority::Verify),
scan_range(350_010..400_001, ScanPriority::Historic),
],
Some(NU6_3_ACTIVATION),
);
assert_eq!(suggestions.first().map(|(p, _, _)| *p), Some(Verify));
}
#[test]
fn pre_activation_backfill_resumes_once_the_window_is_scanned() {
assert_eq!(
suggested(
&[
scan_range(100_000..NU6_3_ACTIVATION, ScanPriority::Historic),
scan_range(NU6_3_ACTIVATION..400_001, ScanPriority::Scanned),
],
Some(NU6_3_ACTIVATION),
),
vec![
(Historic, 100_000, NU6_3_ACTIVATION),
(Scanned, NU6_3_ACTIVATION, 400_001),
],
);
}
#[test]
fn no_relabelling_without_pre_activation_history() {
assert_eq!(
suggested(
&[scan_range(
NU6_3_ACTIVATION..400_001,
ScanPriority::Historic
)],
Some(NU6_3_ACTIVATION),
),
vec![(Historic, NU6_3_ACTIVATION, 400_001)],
);
}
#[test]
fn no_relabelling_without_an_activation_height() {
let entries = [
scan_range(100_000..350_000, ScanPriority::Historic),
scan_range(350_000..400_001, ScanPriority::Historic),
];
assert_eq!(
suggested(&entries, None),
vec![(Historic, 350_000, 400_001), (Historic, 100_000, 350_000),],
);
}
#[test]
fn new_post_activation_work_preempts_an_in_progress_backfill() {
let suggestions = suggested(
&[
scan_range(100_000..200_000, ScanPriority::Historic),
scan_range(200_000..NU6_3_ACTIVATION, ScanPriority::Scanned),
scan_range(NU6_3_ACTIVATION..400_001, ScanPriority::Historic),
],
Some(NU6_3_ACTIVATION),
);
assert_eq!(
suggestions.first(),
Some(&(LatestPoolActivation, NU6_3_ACTIVATION, 400_001)),
);
}
#[test]
fn suggest_scan_ranges_does_not_mutate_the_queue() {
let mut st = pre_nu6_3_birthday_wallet();
st.wallet_mut()
.update_chain_tip(BlockHeight::from_u32(400_000))
.unwrap();
let before = queue_contents(&st);
let suggestions = suggest(
st.wallet().conn(),
Historic,
Some(BlockHeight::from_u32(NU6_3_ACTIVATION)),
)
.unwrap();
assert_eq!(queue_contents(&st), before);
assert!(
!queue_contents(&st)
.iter()
.any(|(_, _, code)| *code == priority_code(&LatestPoolActivation)),
"priority 25 must never be persisted: {:?}",
queue_contents(&st),
);
assert_eq!(
suggestions.first().map(|r| r.priority()),
Some(LatestPoolActivation),
);
}
#[test]
fn queue_rescans_does_not_persist_latest_pool_activation() {
let mut st = pre_nu6_3_birthday_wallet();
st.wallet_mut()
.update_chain_tip(BlockHeight::from_u32(400_000))
.unwrap();
let rescan_start = BlockHeight::from_u32(310_000);
let rescan_end = BlockHeight::from_u32(320_000);
st.wallet_mut()
.db_mut()
.queue_rescans(
NonEmpty {
head: rescan_start..rescan_end,
tail: vec![],
},
LatestPoolActivation,
)
.unwrap();
let contents = queue_contents(&st);
assert!(
contents.iter().any(|&(start, end, code)| {
start == u32::from(rescan_start)
&& end == u32::from(rescan_end)
&& code == priority_code(&Historic)
}),
"the derived priority should be stored as Historic: {contents:?}",
);
assert!(
contents
.iter()
.all(|&(_, _, code)| [0, 10, 20, 30, 40, 50, 60].contains(&code)),
"the queue contains a priority code rejected by the previous release: {contents:?}",
);
}
#[cfg(feature = "orchard")]
#[test]
fn wallet_read_suggest_scan_ranges_derives_from_the_nu6_3_activation_height() {
let mut st = pre_nu6_3_birthday_wallet();
let birthday = u32::from(st.wallet().get_wallet_birthday().unwrap().unwrap());
assert!(birthday < PRIOR_NU6_ACTIVATION);
st.wallet_mut()
.update_chain_tip(BlockHeight::from_u32(400_000))
.unwrap();
let suggestions = st
.wallet()
.suggest_scan_ranges()
.unwrap()
.into_iter()
.map(|r| {
(
r.priority(),
u32::from(r.block_range().start),
u32::from(r.block_range().end),
)
})
.collect::<Vec<_>>();
assert_eq!(
suggestions,
vec![
(LatestPoolActivation, NU6_3_ACTIVATION, 400_001),
(Historic, birthday, NU6_3_ACTIVATION),
],
);
}
#[cfg(not(feature = "orchard"))]
#[test]
fn wallet_read_suggest_scan_ranges_does_not_derive_without_orchard() {
let mut st = pre_nu6_3_birthday_wallet();
let birthday = u32::from(st.wallet().get_wallet_birthday().unwrap().unwrap());
st.wallet_mut()
.update_chain_tip(BlockHeight::from_u32(400_000))
.unwrap();
assert_eq!(
st.wallet()
.suggest_scan_ranges()
.unwrap()
.into_iter()
.map(|r| (
r.priority(),
u32::from(r.block_range().start),
u32::from(r.block_range().end)
))
.collect::<Vec<_>>(),
vec![(Historic, birthday, 400_001)],
);
}
#[test]
fn pre_nu6_3_birthday_wallet_scans_the_ironwood_era_first() {
let mut st = pre_nu6_3_birthday_wallet();
let birthday = u32::from(st.wallet().get_wallet_birthday().unwrap().unwrap());
assert!(birthday < NU6_3_ACTIVATION);
st.wallet_mut()
.update_chain_tip(BlockHeight::from_u32(400_000))
.unwrap();
assert_eq!(
queue_contents(&st),
vec![(birthday, 400_001, priority_code(&Historic))],
);
assert_queue_contiguous(&st);
assert_eq!(
suggest(
st.wallet().conn(),
Historic,
Some(BlockHeight::from_u32(NU6_3_ACTIVATION)),
)
.unwrap()
.into_iter()
.map(|r| (
r.priority(),
u32::from(r.block_range().start),
u32::from(r.block_range().end)
))
.collect::<Vec<_>>(),
vec![
(LatestPoolActivation, NU6_3_ACTIVATION, 400_001),
(Historic, birthday, NU6_3_ACTIVATION),
],
);
}
fn queue_contents(st: &TestState<(), TestDb, LocalNetwork>) -> Vec<(u32, u32, i64)> {
let mut stmt = st
.wallet()
.conn()
.prepare(
"SELECT block_range_start, block_range_end, priority FROM scan_queue
ORDER BY block_range_start",
)
.unwrap();
stmt.query_map([], |r| {
Ok((
r.get::<_, u32>(0)?,
r.get::<_, u32>(1)?,
r.get::<_, i64>(2)?,
))
})
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
}
fn assert_queue_contiguous(st: &TestState<(), TestDb, LocalNetwork>) {
let entries = queue_contents(st);
for pair in entries.windows(2) {
assert_eq!(
pair[0].1, pair[1].0,
"gap in scan queue between {:?} and {:?}",
pair[0], pair[1]
);
}
}
}