Merge pull request #932 from zcash/update_chain_tip-bugs
Fix `update_chain_tip` bugs
This commit is contained in:
commit
4e823d92eb
|
@ -18,6 +18,8 @@ use crate::{
|
||||||
PRUNING_DEPTH, VERIFY_LOOKAHEAD,
|
PRUNING_DEPTH, VERIFY_LOOKAHEAD,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::wallet_birthday;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum InsertOn {
|
enum InsertOn {
|
||||||
Left,
|
Left,
|
||||||
|
@ -715,8 +717,17 @@ pub(crate) fn update_chain_tip<P: consensus::Parameters>(
|
||||||
params: &P,
|
params: &P,
|
||||||
new_tip: BlockHeight,
|
new_tip: BlockHeight,
|
||||||
) -> Result<(), SqliteClientError> {
|
) -> Result<(), SqliteClientError> {
|
||||||
|
// If the caller provided a chain tip that is before Sapling activation, do nothing.
|
||||||
|
let sapling_activation = match params.activation_height(NetworkUpgrade::Sapling) {
|
||||||
|
Some(h) if h <= new_tip => h,
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
// Read the previous max scanned height from the blocks table
|
// Read the previous max scanned height from the blocks table
|
||||||
let prior_tip = block_height_extrema(conn)?.map(|(_, prior_tip)| prior_tip);
|
let max_scanned = block_height_extrema(conn)?.map(|(_, max_scanned)| max_scanned);
|
||||||
|
|
||||||
|
// Read the wallet birthday (if known).
|
||||||
|
let wallet_birthday = wallet_birthday(conn)?;
|
||||||
|
|
||||||
// If the chain tip is below the prior max scanned height, then the caller has caught
|
// If the chain tip is below the prior max scanned height, then the caller has caught
|
||||||
// the chain in the middle of a reorg. Do nothing; the caller will continue using the
|
// the chain in the middle of a reorg. Do nothing; the caller will continue using the
|
||||||
|
@ -730,11 +741,10 @@ pub(crate) fn update_chain_tip<P: consensus::Parameters>(
|
||||||
// We don't check the shard height, as normal usage would have the caller update the
|
// We don't check the shard height, as normal usage would have the caller update the
|
||||||
// shard state prior to this call, so it is possible and expected to be in a situation
|
// shard state prior to this call, so it is possible and expected to be in a situation
|
||||||
// where we should update the tip-related scan ranges but not the shard-related ones.
|
// where we should update the tip-related scan ranges but not the shard-related ones.
|
||||||
if let Some(h) = prior_tip {
|
match max_scanned {
|
||||||
if new_tip < h {
|
Some(h) if new_tip < h => return Ok(()),
|
||||||
return Ok(());
|
_ => (),
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// `ScanRange` uses an exclusive upper bound.
|
// `ScanRange` uses an exclusive upper bound.
|
||||||
let chain_end = new_tip + 1;
|
let chain_end = new_tip + 1;
|
||||||
|
@ -748,82 +758,115 @@ pub(crate) fn update_chain_tip<P: consensus::Parameters>(
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Create a scanning range for the fragment of the last shard leading up to new tip.
|
// Create a scanning range for the fragment of the last shard leading up to new tip.
|
||||||
let shard_entry = shard_start_height
|
let tip_shard_entry = shard_start_height
|
||||||
.filter(|h| h < &chain_end)
|
.filter(|h| h < &chain_end)
|
||||||
.map(|h| ScanRange::from_parts(h..chain_end, ScanPriority::ChainTip));
|
.map(|h| ScanRange::from_parts(h..chain_end, ScanPriority::ChainTip));
|
||||||
|
|
||||||
// Create scanning ranges to either validate potentially invalid blocks at the wallet's view
|
// Create scan ranges to either validate potentially invalid blocks at the wallet's
|
||||||
// of the chain tip, or connect the prior tip to the new tip.
|
// view of the chain tip, or connect the prior tip to the new tip.
|
||||||
let tip_entry = prior_tip.map(|prior_tip| {
|
let tip_entry = max_scanned.map_or_else(
|
||||||
// If we don't have shard metadata, this means we're doing linear scanning, so create a
|
|| {
|
||||||
// scan range from the prior tip to the current tip with `Historic` priority.
|
// No blocks have been scanned, so we need to anchor the start of the new scan
|
||||||
if shard_entry.is_none() {
|
// range to something else.
|
||||||
ScanRange::from_parts(prior_tip..chain_end, ScanPriority::Historic)
|
wallet_birthday.map_or_else(
|
||||||
} else {
|
// We don't have a wallet birthday, which means we have no accounts yet.
|
||||||
// Determine the height to which we expect blocks retrieved from the block source to be stable
|
// We can therefore ignore all blocks up to the chain tip.
|
||||||
// and not subject to being reorg'ed.
|
|| ScanRange::from_parts(sapling_activation..chain_end, ScanPriority::Ignored),
|
||||||
let stable_height = new_tip.saturating_sub(PRUNING_DEPTH);
|
// We have a wallet birthday, so mark all blocks between that and the
|
||||||
|
// chain tip as `Historic` (performing wallet recovery).
|
||||||
|
|wallet_birthday| {
|
||||||
|
ScanRange::from_parts(wallet_birthday..chain_end, ScanPriority::Historic)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|max_scanned| {
|
||||||
|
// The scan range starts at the block after the max scanned height. Since
|
||||||
|
// `scan_cached_blocks` retrieves the metadata for the block being connected to
|
||||||
|
// (if it exists), the connectivity of the scan range to the max scanned block
|
||||||
|
// will always be checked if relevant.
|
||||||
|
let min_unscanned = max_scanned + 1;
|
||||||
|
|
||||||
// If the wallet's prior tip is above the stable height, prioritize the range between
|
// If we don't have shard metadata, this means we're doing linear scanning, so
|
||||||
// it and the new tip as `ChainTip`. Otherwise, prioritize the `VERIFY_LOOKAHEAD`
|
// create a scan range from the prior tip to the current tip with `Historic`
|
||||||
// blocks above the wallet's prior tip as `Verify`. Since `scan_cached_blocks`
|
// priority.
|
||||||
// retrieves the metadata for the block being connected to, the connectivity to the
|
if tip_shard_entry.is_none() {
|
||||||
// prior tip will always be checked. Since `Verify` ranges have maximum priority, even
|
ScanRange::from_parts(min_unscanned..chain_end, ScanPriority::Historic)
|
||||||
// if the block source begins downloading blocks from the shard scan range (which ends
|
|
||||||
// at the stable height) the scanner should not attempt to scan those blocks until the
|
|
||||||
// tip range has been completely checked and any required rewinds have been performed.
|
|
||||||
if prior_tip >= stable_height {
|
|
||||||
// This may overlap the `shard_entry` range and if so will be coalesced with it.
|
|
||||||
ScanRange::from_parts(prior_tip..chain_end, ScanPriority::ChainTip)
|
|
||||||
} else {
|
} else {
|
||||||
// The prior tip is in the range that we now expect to be stable, so we need to verify
|
// Determine the height to which we expect new blocks retrieved from the
|
||||||
// and advance it up to at most the stable height. The shard entry will then cover
|
// block source to be stable and not subject to being reorg'ed.
|
||||||
// the range to the new tip at the lower `ChainTip` priority.
|
let stable_height = new_tip.saturating_sub(PRUNING_DEPTH);
|
||||||
ScanRange::from_parts(
|
|
||||||
prior_tip..min(stable_height, prior_tip + VERIFY_LOOKAHEAD),
|
// If the wallet's max scanned height is above the stable height,
|
||||||
ScanPriority::Verify,
|
// prioritize the range between it and the new tip as `ChainTip`.
|
||||||
)
|
if max_scanned > stable_height {
|
||||||
|
// We are in the steady-state case, where a wallet is close to the
|
||||||
|
// chain tip and just needs to catch up.
|
||||||
|
//
|
||||||
|
// This overlaps the `tip_shard_entry` range and so will be coalesced
|
||||||
|
// with it.
|
||||||
|
ScanRange::from_parts(min_unscanned..chain_end, ScanPriority::ChainTip)
|
||||||
|
} else {
|
||||||
|
// In this case, the max scanned height is considered stable relative
|
||||||
|
// to the chain tip. However, it may be stable or unstable relative to
|
||||||
|
// the prior chain tip, which we could determine by looking up the
|
||||||
|
// prior chain tip height from the scan queue. For simplicity we merge
|
||||||
|
// these two cases together, and proceed as though the max scanned
|
||||||
|
// block is unstable relative to the prior chain tip.
|
||||||
|
//
|
||||||
|
// To confirm its stability, prioritize the `VERIFY_LOOKAHEAD` blocks
|
||||||
|
// above the max scanned height as `Verify`:
|
||||||
|
//
|
||||||
|
// - We use `Verify` to ensure that a connectivity check is performed,
|
||||||
|
// along with any required rewinds, before any `ChainTip` ranges
|
||||||
|
// (from this or any prior `update_chain_tip` call) are scanned.
|
||||||
|
//
|
||||||
|
// - We prioritize `VERIFY_LOOKAHEAD` blocks because this is expected
|
||||||
|
// to be 12.5 minutes, within which it is reasonable for a user to
|
||||||
|
// have potentially received a transaction (if they opened their
|
||||||
|
// wallet to provide an address to someone else, or spent their own
|
||||||
|
// funds creating a change output), without necessarily having left
|
||||||
|
// their wallet open long enough for the transaction to be mined and
|
||||||
|
// the corresponding block to be scanned.
|
||||||
|
//
|
||||||
|
// - We limit the range to at most the stable region, to prevent any
|
||||||
|
// `Verify` ranges from being susceptible to reorgs, and potentially
|
||||||
|
// interfering with subsequent `Verify` ranges defined by future
|
||||||
|
// calls to `update_chain_tip`. Any gap between `stable_height` and
|
||||||
|
// `shard_start_height` will be filled by the scan range merging
|
||||||
|
// logic with a `Historic` range.
|
||||||
|
//
|
||||||
|
// If `max_scanned == stable_height` then this is a zero-length range.
|
||||||
|
// In this case, any non-empty `(stable_height+1)..shard_start_height`
|
||||||
|
// will be marked `Historic`, minimising the prioritised blocks at the
|
||||||
|
// chain tip and allowing for other ranges (for example, `FoundNote`)
|
||||||
|
// to take priority.
|
||||||
|
ScanRange::from_parts(
|
||||||
|
min_unscanned..min(stable_height + 1, min_unscanned + VERIFY_LOOKAHEAD),
|
||||||
|
ScanPriority::Verify,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
if let Some(entry) = &shard_entry {
|
if let Some(entry) = &tip_shard_entry {
|
||||||
debug!("{} will update latest shard", entry);
|
debug!("{} will update latest shard", entry);
|
||||||
}
|
}
|
||||||
if let Some(entry) = &tip_entry {
|
debug!("{} will connect prior scanned state to new tip", tip_entry);
|
||||||
debug!("{} will connect prior tip to new tip", entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
let query_range = match (shard_entry.as_ref(), tip_entry.as_ref()) {
|
let query_range = match tip_shard_entry.as_ref() {
|
||||||
(Some(se), Some(te)) => Some(Range {
|
Some(se) => Range {
|
||||||
start: min(se.block_range().start, te.block_range().start),
|
start: min(se.block_range().start, tip_entry.block_range().start),
|
||||||
end: max(se.block_range().end, te.block_range().end),
|
end: max(se.block_range().end, tip_entry.block_range().end),
|
||||||
}),
|
},
|
||||||
(Some(se), None) => Some(se.block_range().clone()),
|
None => tip_entry.block_range().clone(),
|
||||||
(None, Some(te)) => Some(te.block_range().clone()),
|
|
||||||
(None, None) => None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(query_range) = query_range {
|
replace_queue_entries::<SqliteClientError>(
|
||||||
replace_queue_entries::<SqliteClientError>(
|
conn,
|
||||||
conn,
|
&query_range,
|
||||||
&query_range,
|
tip_shard_entry.into_iter().chain(Some(tip_entry)),
|
||||||
shard_entry.into_iter().chain(tip_entry.into_iter()),
|
false,
|
||||||
false,
|
)?;
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
// If we have neither shard data nor any existing block data in the database, we should also
|
|
||||||
// have no existing scan queue entries and can fall back to linear scanning from Sapling
|
|
||||||
// activation.
|
|
||||||
if let Some(sapling_activation) = params.activation_height(NetworkUpgrade::Sapling) {
|
|
||||||
// If the caller provided a chain tip that is before Sapling activation, do
|
|
||||||
// nothing.
|
|
||||||
if sapling_activation < chain_end {
|
|
||||||
let scan_range =
|
|
||||||
ScanRange::from_parts(sapling_activation..chain_end, ScanPriority::Historic);
|
|
||||||
insert_queue_entries(conn, Some(scan_range).iter())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -834,6 +877,7 @@ mod tests {
|
||||||
|
|
||||||
use incrementalmerkletree::{frontier::Frontier, Hashable, Level, Position};
|
use incrementalmerkletree::{frontier::Frontier, Hashable, Level, Position};
|
||||||
|
|
||||||
|
use secrecy::SecretVec;
|
||||||
use zcash_client_backend::data_api::{
|
use zcash_client_backend::data_api::{
|
||||||
chain::CommitmentTreeRoot,
|
chain::CommitmentTreeRoot,
|
||||||
scanning::{ScanPriority, ScanRange},
|
scanning::{ScanPriority, ScanRange},
|
||||||
|
@ -844,12 +888,14 @@ mod tests {
|
||||||
consensus::{BlockHeight, NetworkUpgrade, Parameters},
|
consensus::{BlockHeight, NetworkUpgrade, Parameters},
|
||||||
sapling::Node,
|
sapling::Node,
|
||||||
transaction::components::Amount,
|
transaction::components::Amount,
|
||||||
|
zip32::DiversifiableFullViewingKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::SqliteClientError,
|
error::SqliteClientError,
|
||||||
testing::{AddressType, TestBuilder},
|
testing::{AddressType, BlockCache, TestBuilder, TestState},
|
||||||
wallet::scanning::{insert_queue_entries, replace_queue_entries, suggest_scan_ranges},
|
wallet::scanning::{insert_queue_entries, replace_queue_entries, suggest_scan_ranges},
|
||||||
|
VERIFY_LOOKAHEAD,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{join_nonoverlapping, Joined, RangeOrdering, SpanningTree};
|
use super::{join_nonoverlapping, Joined, RangeOrdering, SpanningTree};
|
||||||
|
@ -1047,6 +1093,30 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spanning_tree_insert_gaps() {
|
||||||
|
use ScanPriority::*;
|
||||||
|
|
||||||
|
let t = spanning_tree(&[(0..3, Historic), (6..8, ChainTip)]).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
t.into_vec(),
|
||||||
|
vec![scan_range(0..6, Historic), scan_range(6..8, ChainTip),]
|
||||||
|
);
|
||||||
|
|
||||||
|
let t = spanning_tree(&[(0..3, Historic), (3..4, Verify), (6..8, ChainTip)]).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
t.into_vec(),
|
||||||
|
vec![
|
||||||
|
scan_range(0..3, Historic),
|
||||||
|
scan_range(3..4, Verify),
|
||||||
|
scan_range(4..6, Historic),
|
||||||
|
scan_range(6..8, ChainTip),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spanning_tree_insert_rfd_span() {
|
fn spanning_tree_insert_rfd_span() {
|
||||||
use ScanPriority::*;
|
use ScanPriority::*;
|
||||||
|
@ -1314,18 +1384,20 @@ mod tests {
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
st.wallet().suggest_scan_ranges(),
|
st.wallet().suggest_scan_ranges(),
|
||||||
Ok(scan_ranges) if scan_ranges == vec![
|
Ok(scan_ranges) if scan_ranges == vec![
|
||||||
scan_range((sap_active + 319)..(sap_active + 329), Verify),
|
scan_range((sap_active + 320)..(sap_active + 330), Verify),
|
||||||
scan_range((sap_active + 329)..(sap_active + 451), ChainTip),
|
scan_range((sap_active + 330)..(sap_active + 451), ChainTip),
|
||||||
scan_range((sap_active + 300)..(sap_active + 310), ChainTip)
|
scan_range((sap_active + 300)..(sap_active + 310), ChainTip)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn test_with_canopy_birthday() -> (
|
||||||
fn create_account_creates_ignored_range() {
|
TestState<BlockCache>,
|
||||||
use ScanPriority::*;
|
DiversifiableFullViewingKey,
|
||||||
|
AccountBirthday,
|
||||||
let mut st = TestBuilder::new()
|
u32,
|
||||||
|
) {
|
||||||
|
let st = TestBuilder::new()
|
||||||
.with_block_cache()
|
.with_block_cache()
|
||||||
.with_test_account(|network| {
|
.with_test_account(|network| {
|
||||||
// We use Canopy activation as an arbitrary birthday height that's greater than Sapling
|
// We use Canopy activation as an arbitrary birthday height that's greater than Sapling
|
||||||
|
@ -1346,27 +1418,190 @@ mod tests {
|
||||||
let dfvk = st.test_account_sapling().unwrap();
|
let dfvk = st.test_account_sapling().unwrap();
|
||||||
let sap_active = st.sapling_activation_height();
|
let sap_active = st.sapling_activation_height();
|
||||||
|
|
||||||
|
(st, dfvk, birthday, sap_active.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_account_creates_ignored_range() {
|
||||||
|
use ScanPriority::*;
|
||||||
|
|
||||||
|
let (st, _, birthday, sap_active) = test_with_canopy_birthday();
|
||||||
|
let birthday_height = birthday.height().into();
|
||||||
|
|
||||||
let expected = vec![
|
let expected = vec![
|
||||||
// The range up to the wallet's birthday height is ignored.
|
// The range up to the wallet's birthday height is ignored.
|
||||||
scan_range(u32::from(sap_active)..u32::from(birthday.height()), Ignored),
|
scan_range(sap_active..birthday_height, Ignored),
|
||||||
|
];
|
||||||
|
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_chain_tip_before_create_account() {
|
||||||
|
use ScanPriority::*;
|
||||||
|
|
||||||
|
let mut st = TestBuilder::new().with_block_cache().build();
|
||||||
|
let sap_active = st.sapling_activation_height();
|
||||||
|
|
||||||
|
// Update the chain tip.
|
||||||
|
let new_tip = sap_active + 1000;
|
||||||
|
st.wallet_mut().update_chain_tip(new_tip).unwrap();
|
||||||
|
let chain_end = u32::from(new_tip + 1);
|
||||||
|
|
||||||
|
let expected = vec![
|
||||||
|
// The range up to the chain end is ignored.
|
||||||
|
scan_range(sap_active.into()..chain_end, Ignored),
|
||||||
];
|
];
|
||||||
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
|
|
||||||
// Set up some shard root history before the wallet birthday
|
// Now add an account.
|
||||||
|
let wallet_birthday = sap_active + 500;
|
||||||
|
st.wallet_mut()
|
||||||
|
.create_account(
|
||||||
|
&SecretVec::new(vec![0; 32]),
|
||||||
|
AccountBirthday::from_parts(wallet_birthday, Frontier::empty(), None),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let expected = vec![
|
||||||
|
// The account's birthday onward is marked for recovery.
|
||||||
|
scan_range(wallet_birthday.into()..chain_end, Historic),
|
||||||
|
// The range up to the wallet's birthday height is ignored.
|
||||||
|
scan_range(sap_active.into()..wallet_birthday.into(), Ignored),
|
||||||
|
];
|
||||||
|
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_chain_tip_with_no_subtree_roots() {
|
||||||
|
use ScanPriority::*;
|
||||||
|
|
||||||
|
let (mut st, _, birthday, sap_active) = test_with_canopy_birthday();
|
||||||
|
|
||||||
|
// Set up the following situation:
|
||||||
|
//
|
||||||
|
// prior_tip new_tip
|
||||||
|
// |<--- 500 --->|
|
||||||
|
// wallet_birthday
|
||||||
|
let prior_tip = birthday.height();
|
||||||
|
let wallet_birthday = birthday.height().into();
|
||||||
|
|
||||||
|
// Update the chain tip.
|
||||||
|
let new_tip = prior_tip + 500;
|
||||||
|
st.wallet_mut().update_chain_tip(new_tip).unwrap();
|
||||||
|
let chain_end = u32::from(new_tip + 1);
|
||||||
|
|
||||||
|
// Verify that the suggested scan ranges match what is expected.
|
||||||
|
let expected = vec![
|
||||||
|
// The wallet's birthday onward is marked for recovery.
|
||||||
|
scan_range(wallet_birthday..chain_end, Historic),
|
||||||
|
// The range below the wallet's birthday height is ignored.
|
||||||
|
scan_range(sap_active..wallet_birthday, Ignored),
|
||||||
|
];
|
||||||
|
|
||||||
|
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_chain_tip_when_never_scanned() {
|
||||||
|
use ScanPriority::*;
|
||||||
|
|
||||||
|
let (mut st, _, birthday, sap_active) = test_with_canopy_birthday();
|
||||||
|
|
||||||
|
// Set up the following situation:
|
||||||
|
//
|
||||||
|
// last_shard_start prior_tip new_tip
|
||||||
|
// |<----- 1000 ----->|<--- 500 --->|
|
||||||
|
// wallet_birthday
|
||||||
|
let prior_tip_height = birthday.height();
|
||||||
|
|
||||||
|
// Set up some shard root history before the wallet birthday.
|
||||||
|
let last_shard_start = birthday.height() - 1000;
|
||||||
st.wallet_mut()
|
st.wallet_mut()
|
||||||
.put_sapling_subtree_roots(
|
.put_sapling_subtree_roots(
|
||||||
0,
|
0,
|
||||||
&[CommitmentTreeRoot::from_parts(
|
&[CommitmentTreeRoot::from_parts(
|
||||||
birthday.height() - 1000,
|
last_shard_start,
|
||||||
// fake a hash, the value doesn't matter
|
// fake a hash, the value doesn't matter
|
||||||
Node::empty_leaf(),
|
Node::empty_leaf(),
|
||||||
)],
|
)],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// Update the chain tip.
|
||||||
|
let tip_height = prior_tip_height + 500;
|
||||||
|
st.wallet_mut().update_chain_tip(tip_height).unwrap();
|
||||||
|
let chain_end = u32::from(tip_height + 1);
|
||||||
|
|
||||||
|
// Verify that the suggested scan ranges match what is expected.
|
||||||
|
let expected = vec![
|
||||||
|
// The entire last (incomplete) shard's range is marked for catching up to the
|
||||||
|
// chain tip, to ensure that if any notes are discovered after the wallet's
|
||||||
|
// birthday, they will be spendable.
|
||||||
|
scan_range(last_shard_start.into()..chain_end, ChainTip),
|
||||||
|
// The range below the last shard is ignored.
|
||||||
|
scan_range(sap_active..last_shard_start.into(), Ignored),
|
||||||
|
];
|
||||||
|
|
||||||
|
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_chain_tip_unstable_max_scanned() {
|
||||||
|
use ScanPriority::*;
|
||||||
|
|
||||||
|
let (mut st, dfvk, birthday, sap_active) = test_with_canopy_birthday();
|
||||||
|
|
||||||
|
// Set up the following situation:
|
||||||
|
//
|
||||||
|
// prior_tip new_tip
|
||||||
|
// |<--- 500 --->|<- 40 ->|<-- 70 -->|<- 20 ->|
|
||||||
|
// wallet_birthday max_scanned last_shard_start
|
||||||
|
//
|
||||||
|
let max_scanned = birthday.height() + 500;
|
||||||
|
let prior_tip = max_scanned + 40;
|
||||||
|
|
||||||
|
// Set up some shard root history before the wallet birthday.
|
||||||
|
let second_to_last_shard_start = birthday.height() - 1000;
|
||||||
|
st.wallet_mut()
|
||||||
|
.put_sapling_subtree_roots(
|
||||||
|
0,
|
||||||
|
&[CommitmentTreeRoot::from_parts(
|
||||||
|
second_to_last_shard_start,
|
||||||
|
// fake a hash, the value doesn't matter
|
||||||
|
Node::empty_leaf(),
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Set up prior chain state. This simulates us having imported a wallet
|
||||||
|
// with a birthday 520 blocks below the chain tip.
|
||||||
|
st.wallet_mut().update_chain_tip(prior_tip).unwrap();
|
||||||
|
|
||||||
|
// Verify that the suggested scan ranges match what is expected.
|
||||||
|
let expected = vec![
|
||||||
|
// The second-to-last shard is currently the last shard, so it is marked for
|
||||||
|
// scanning to catch up to the prior chain tip. This includes heights prior to
|
||||||
|
// the wallet's birthday, because the wallet doesn't know that it already has
|
||||||
|
// enough data from the initial frontier to avoid having to scan this range.
|
||||||
|
scan_range(
|
||||||
|
second_to_last_shard_start.into()..(prior_tip + 1).into(),
|
||||||
|
ChainTip,
|
||||||
|
),
|
||||||
|
// The range below the second-to-last shard is ignored.
|
||||||
|
scan_range(sap_active..second_to_last_shard_start.into(), Ignored),
|
||||||
|
];
|
||||||
|
|
||||||
|
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
|
||||||
|
// Now, scan the max scanned block.
|
||||||
st.generate_block_at(
|
st.generate_block_at(
|
||||||
birthday.height(),
|
max_scanned,
|
||||||
BlockHash([0u8; 32]),
|
BlockHash([0u8; 32]),
|
||||||
&dfvk,
|
&dfvk,
|
||||||
AddressType::DefaultExternal,
|
AddressType::DefaultExternal,
|
||||||
|
@ -1375,42 +1610,150 @@ mod tests {
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
st.scan_cached_blocks(birthday.height(), 1);
|
st.scan_cached_blocks(max_scanned, 1);
|
||||||
|
|
||||||
// Update the chain tip
|
// Now simulate shutting down, and then restarting 90 blocks later, after a shard
|
||||||
let tip_height = st
|
// has been completed.
|
||||||
.wallet()
|
let last_shard_start = prior_tip + 70;
|
||||||
.params
|
st.wallet_mut()
|
||||||
.activation_height(NetworkUpgrade::Nu5)
|
.put_sapling_subtree_roots(
|
||||||
|
0,
|
||||||
|
&[CommitmentTreeRoot::from_parts(
|
||||||
|
last_shard_start,
|
||||||
|
// fake a hash, the value doesn't matter
|
||||||
|
Node::empty_leaf(),
|
||||||
|
)],
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
st.wallet_mut().update_chain_tip(tip_height).unwrap();
|
|
||||||
|
let new_tip = last_shard_start + 20;
|
||||||
|
st.wallet_mut().update_chain_tip(new_tip).unwrap();
|
||||||
|
let chain_end = u32::from(new_tip + 1);
|
||||||
|
|
||||||
// Verify that the suggested scan ranges match what is expected
|
// Verify that the suggested scan ranges match what is expected
|
||||||
let expected = vec![
|
let expected = vec![
|
||||||
// The birthday height is the "first to be scanned" (as the wallet birthday),
|
// The max scanned block's connectivity is verified by scanning the next 10 blocks.
|
||||||
// so we verify 10 blocks starting at that height.
|
|
||||||
scan_range(
|
scan_range(
|
||||||
u32::from(birthday.height())..u32::from(birthday.height() + 10),
|
(max_scanned + 1).into()..(max_scanned + 1 + VERIFY_LOOKAHEAD).into(),
|
||||||
Verify,
|
Verify,
|
||||||
),
|
),
|
||||||
// The remainder of the shard after the verify segment is required in order to make
|
// The last shard needs to catch up to the chain tip in order to make notes spendable.
|
||||||
// notes spendable, so it has priority `ChainTip`
|
scan_range(last_shard_start.into()..chain_end, ChainTip),
|
||||||
|
// The range between the verification blocks and the prior tip is still in the queue.
|
||||||
scan_range(
|
scan_range(
|
||||||
u32::from(birthday.height() + 10)..u32::from(tip_height + 1),
|
(max_scanned + 1 + VERIFY_LOOKAHEAD).into()..(prior_tip + 1).into(),
|
||||||
ChainTip,
|
ChainTip,
|
||||||
),
|
),
|
||||||
// The remainder of the shard prior to the birthday height must be scanned because the
|
// The remainder of the second-to-last shard's range is still in the queue.
|
||||||
// wallet doesn't know that it already has enough data from the initial frontier to
|
|
||||||
// avoid having to scan this range.
|
|
||||||
scan_range(
|
scan_range(
|
||||||
u32::from(birthday.height() - 1000)..u32::from(birthday.height()),
|
second_to_last_shard_start.into()..max_scanned.into(),
|
||||||
ChainTip,
|
ChainTip,
|
||||||
),
|
),
|
||||||
// The range below the wallet's birthday height is ignored
|
// The gap between the prior tip and the last shard is deferred as low priority.
|
||||||
|
scan_range((prior_tip + 1).into()..last_shard_start.into(), Historic),
|
||||||
|
// The max scanned block itself is left as-is.
|
||||||
|
scan_range(max_scanned.into()..(max_scanned + 1).into(), Scanned),
|
||||||
|
// The range below the second-to-last shard is ignored.
|
||||||
|
scan_range(sap_active..second_to_last_shard_start.into(), Ignored),
|
||||||
|
];
|
||||||
|
|
||||||
|
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_chain_tip_stable_max_scanned() {
|
||||||
|
use ScanPriority::*;
|
||||||
|
|
||||||
|
let (mut st, dfvk, birthday, sap_active) = test_with_canopy_birthday();
|
||||||
|
|
||||||
|
// Set up the following situation:
|
||||||
|
//
|
||||||
|
// prior_tip new_tip
|
||||||
|
// |<--- 500 --->|<- 20 ->|<-- 50 -->|<- 20 ->|
|
||||||
|
// wallet_birthday max_scanned last_shard_start
|
||||||
|
//
|
||||||
|
let max_scanned = birthday.height() + 500;
|
||||||
|
let prior_tip = max_scanned + 20;
|
||||||
|
|
||||||
|
// Set up some shard root history before the wallet birthday.
|
||||||
|
let second_to_last_shard_start = birthday.height() - 1000;
|
||||||
|
st.wallet_mut()
|
||||||
|
.put_sapling_subtree_roots(
|
||||||
|
0,
|
||||||
|
&[CommitmentTreeRoot::from_parts(
|
||||||
|
second_to_last_shard_start,
|
||||||
|
// fake a hash, the value doesn't matter
|
||||||
|
Node::empty_leaf(),
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Set up prior chain state. This simulates us having imported a wallet
|
||||||
|
// with a birthday 520 blocks below the chain tip.
|
||||||
|
st.wallet_mut().update_chain_tip(prior_tip).unwrap();
|
||||||
|
|
||||||
|
// Verify that the suggested scan ranges match what is expected.
|
||||||
|
let expected = vec![
|
||||||
|
// The second-to-last shard is currently the last shard, so it is marked for
|
||||||
|
// scanning to catch up to the prior chain tip. This includes heights prior to
|
||||||
|
// the wallet's birthday, because the wallet doesn't know that it already has
|
||||||
|
// enough data from the initial frontier to avoid having to scan this range.
|
||||||
scan_range(
|
scan_range(
|
||||||
u32::from(sap_active)..u32::from(birthday.height() - 1000),
|
second_to_last_shard_start.into()..(prior_tip + 1).into(),
|
||||||
Ignored,
|
ChainTip,
|
||||||
),
|
),
|
||||||
|
// The range below the second-to-last shard is ignored.
|
||||||
|
scan_range(sap_active..second_to_last_shard_start.into(), Ignored),
|
||||||
|
];
|
||||||
|
|
||||||
|
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
|
||||||
|
// Now, scan the max scanned block.
|
||||||
|
st.generate_block_at(
|
||||||
|
max_scanned,
|
||||||
|
BlockHash([0u8; 32]),
|
||||||
|
&dfvk,
|
||||||
|
AddressType::DefaultExternal,
|
||||||
|
Amount::const_from_i64(10000),
|
||||||
|
u64::from(birthday.sapling_frontier().value().unwrap().position() + 1)
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
st.scan_cached_blocks(max_scanned, 1);
|
||||||
|
|
||||||
|
// Now simulate shutting down, and then restarting 70 blocks later, after a shard
|
||||||
|
// has been completed.
|
||||||
|
let last_shard_start = prior_tip + 50;
|
||||||
|
st.wallet_mut()
|
||||||
|
.put_sapling_subtree_roots(
|
||||||
|
0,
|
||||||
|
&[CommitmentTreeRoot::from_parts(
|
||||||
|
last_shard_start,
|
||||||
|
// fake a hash, the value doesn't matter
|
||||||
|
Node::empty_leaf(),
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let new_tip = last_shard_start + 20;
|
||||||
|
st.wallet_mut().update_chain_tip(new_tip).unwrap();
|
||||||
|
let chain_end = u32::from(new_tip + 1);
|
||||||
|
|
||||||
|
// Verify that the suggested scan ranges match what is expected.
|
||||||
|
let expected = vec![
|
||||||
|
// The blocks after the max scanned block up to the chain tip are prioritised.
|
||||||
|
scan_range((max_scanned + 1).into()..chain_end, ChainTip),
|
||||||
|
// The remainder of the second-to-last shard's range is still in the queue.
|
||||||
|
scan_range(
|
||||||
|
second_to_last_shard_start.into()..max_scanned.into(),
|
||||||
|
ChainTip,
|
||||||
|
),
|
||||||
|
// The max scanned block itself is left as-is.
|
||||||
|
scan_range(max_scanned.into()..(max_scanned + 1).into(), Scanned),
|
||||||
|
// The range below the second-to-last shard is ignored.
|
||||||
|
scan_range(sap_active..second_to_last_shard_start.into(), Ignored),
|
||||||
];
|
];
|
||||||
|
|
||||||
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
|
||||||
|
|
Loading…
Reference in New Issue