1use std::collections::{BTreeMap, HashMap};
4use std::convert::Infallible;
5use std::future::Future;
6use std::ops::{Bound, Range};
7use std::sync::atomic::{self, AtomicBool, AtomicU8, AtomicU32};
8use std::sync::{Arc, OnceLock};
9use std::time::{Duration, Instant, SystemTime};
10
11use shardtree::ShardTree;
12use shardtree::store::memory::MemoryShardStore;
13use tokio::sync::{RwLock, mpsc, watch};
14
15use incrementalmerkletree::{Marking, Retention};
16use orchard::tree::MerkleHashOrchard;
17use shardtree::store::ShardStore;
18use zcash_keys::keys::UnifiedFullViewingKey;
19use zcash_primitives::transaction::{Transaction, TxId};
20use zcash_protocol::consensus::{self, BlockHeight};
21use zcash_protocol::{PoolType, ShieldedPool};
22use zingo_netutils::lightwallet_protocol::{RawTransaction, SubtreeRoot};
23use zingo_netutils::{Indexer, TransparentIndexer};
24use zip32::AccountId;
25
26use zingo_status::confirmation_status::ConfirmationStatus;
27
28use crate::client::{self, FetchRequest};
29use crate::config::{PerformanceLevel, SyncConfig};
30use crate::error::{
31 ContinuityError, MempoolError, ScanError, ServerError, SyncError, SyncModeError,
32 SyncStatusError,
33};
34use crate::keys::transparent::TransparentAddressId;
35use crate::scan::ScanResults;
36use crate::scan::task::{ScanLoad, Scanner, ScannerState, TaskId};
37use crate::scan::transactions::scan_transaction;
38use crate::shardtree_ext::{RollbackOutcome, ShardTreeExt};
39use crate::sync::state::{ScanResultsStanding, VerifyEnd};
40use crate::wallet::traits::{
41 SyncBlocks, SyncNullifiers, SyncOutPoints, SyncShardTrees, SyncTransactions, SyncWallet,
42};
43use crate::wallet::{
44 KeyIdInterface, NoteInterface, NullifierMap, OutputId, OutputInterface, PoolActivation,
45 ScanTarget, SyncMode, SyncState, WalletBlock, WalletTransaction,
46};
47use crate::witness::{ANCHOR_RETENTION_INTERVALS, LocatedTreeData};
48
49use crate::witness;
50
51pub(crate) mod spend;
52pub(crate) mod state;
53pub(crate) mod transparent;
54pub mod truncate;
55
56pub const MAX_REORG_ALLOWANCE: u32 = 100;
71
72pub const SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE: u32 = MAX_REORG_ALLOWANCE + 1;
74
75pub const MAX_SHARDTREE_CHECKPOINTS: u32 =
77 SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE + ANCHOR_RETENTION_INTERVALS;
78
79const VERIFY_BLOCK_RANGE_SIZE: u32 = 10;
80
81pub const CHECK_NEW_BLOCKS_INTERVAL: u64 = 10;
84
85#[derive(Debug, Clone)]
87#[allow(missing_docs)]
88pub struct SyncStatus {
89 pub scan_ranges: Vec<ScanRange>,
90 pub sync_start_height: BlockHeight,
91 pub session_blocks_scanned: u32,
92 pub total_blocks_scanned: u32,
93 pub total_blocks: u32,
94 pub session_sapling_outputs_scanned: u32,
95 pub total_sapling_outputs_scanned: u32,
96 pub session_orchard_outputs_scanned: u32,
97 pub total_orchard_outputs_scanned: u32,
98 pub session_ironwood_outputs_scanned: u32,
99 pub total_ironwood_outputs_scanned: u32,
100 pub total_outputs_scanned: u64,
105 pub total_outputs: u64,
110}
111
112impl SyncStatus {
113 pub fn is_complete(&self) -> bool {
121 self.sync_start_height != 0.into()
122 && self
123 .scan_ranges
124 .iter()
125 .all(|scan_range| scan_range.priority() == ScanPriority::Scanned)
126 }
127
128 pub fn nullifier_retrieval_pending(&self) -> bool {
161 self.scan_ranges
162 .iter()
163 .map(ScanRange::priority)
164 .any(ScanPriority::awaits_nullifier_retrieval)
165 }
166}
167
168#[derive(Debug, Clone)]
170#[allow(missing_docs)]
171pub struct SyncResult {
172 pub sync_start_height: BlockHeight,
173 pub sync_end_height: BlockHeight,
174 pub blocks_scanned: u32,
175 pub sapling_outputs_scanned: u32,
176 pub orchard_outputs_scanned: u32,
177 pub ironwood_outputs_scanned: u32,
178 pub total_outputs_scanned: u64,
179 pub total_outputs: u64,
180 pub nullifier_retrieval_pending: bool,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
185pub enum ScanPriority {
186 RefetchingNullifiers,
188 Scanning,
190 Scanned,
192 ScannedWithoutMapping,
197 Historic,
199 OpenAdjacent,
201 FoundNote,
203 ChainTip,
205 Verify,
208}
209
210impl ScanPriority {
211 pub fn is_scanned(self) -> bool {
216 matches!(
217 self,
218 ScanPriority::Scanned
219 | ScanPriority::ScannedWithoutMapping
220 | ScanPriority::RefetchingNullifiers
221 )
222 }
223
224 pub fn awaits_nullifier_retrieval(self) -> bool {
228 matches!(
229 self,
230 ScanPriority::ScannedWithoutMapping | ScanPriority::RefetchingNullifiers
231 )
232 }
233
234 pub fn in_flight(self) -> ScanPriority {
236 if self == ScanPriority::ScannedWithoutMapping {
237 ScanPriority::RefetchingNullifiers
238 } else {
239 ScanPriority::Scanning
240 }
241 }
242}
243
244pub(crate) fn overlaps(first: &Range<BlockHeight>, second: &Range<BlockHeight>) -> bool {
246 first.start < second.end && second.start < first.end
247}
248
249pub(crate) fn held_at(
251 scan_ranges: &[ScanRange],
252 priority: ScanPriority,
253) -> impl Iterator<Item = &ScanRange> {
254 scan_ranges
255 .iter()
256 .filter(move |scan_range| scan_range.priority() == priority)
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct ScanRange {
262 block_range: Range<BlockHeight>,
263 priority: ScanPriority,
264}
265
266impl std::fmt::Display for ScanRange {
267 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268 write!(
269 f,
270 "{:?}({}..{})",
271 self.priority, self.block_range.start, self.block_range.end,
272 )
273 }
274}
275
276impl ScanRange {
277 #[must_use]
279 pub fn from_parts(block_range: Range<BlockHeight>, priority: ScanPriority) -> Self {
280 assert!(
281 block_range.end >= block_range.start,
282 "{block_range:?} is invalid for ScanRange({priority:?})",
283 );
284 ScanRange {
285 block_range,
286 priority,
287 }
288 }
289
290 #[must_use]
292 pub fn block_range(&self) -> &Range<BlockHeight> {
293 &self.block_range
294 }
295
296 #[must_use]
298 pub fn priority(&self) -> ScanPriority {
299 self.priority
300 }
301
302 #[must_use]
304 pub fn is_empty(&self) -> bool {
305 self.block_range.is_empty()
306 }
307
308 #[must_use]
310 pub fn encloses(&self, block_range: &Range<BlockHeight>) -> bool {
311 self.block_range.start <= block_range.start && block_range.end <= self.block_range.end
312 }
313
314 #[must_use]
316 pub fn overlaps(&self, block_range: &Range<BlockHeight>) -> bool {
317 overlaps(&self.block_range, block_range)
318 }
319
320 #[must_use]
322 pub fn len(&self) -> usize {
323 usize::try_from(u32::from(self.block_range.end) - u32::from(self.block_range.start))
324 .expect("due to number of max blocks should always be valid usize")
325 }
326
327 #[must_use]
331 pub fn truncate_start(&self, block_height: BlockHeight) -> Option<Self> {
332 if block_height >= self.block_range.end || self.is_empty() {
333 None
334 } else {
335 Some(ScanRange {
336 block_range: self.block_range.start.max(block_height)..self.block_range.end,
337 priority: self.priority,
338 })
339 }
340 }
341
342 #[must_use]
346 pub fn truncate_end(&self, block_height: BlockHeight) -> Option<Self> {
347 if block_height <= self.block_range.start || self.is_empty() {
348 None
349 } else {
350 Some(ScanRange {
351 block_range: self.block_range.start..self.block_range.end.min(block_height),
352 priority: self.priority,
353 })
354 }
355 }
356
357 #[must_use]
361 pub fn split_at(&self, p: BlockHeight) -> Option<(Self, Self)> {
362 (p > self.block_range.start && p < self.block_range.end).then_some((
363 ScanRange {
364 block_range: self.block_range.start..p,
365 priority: self.priority,
366 },
367 ScanRange {
368 block_range: p..self.block_range.end,
369 priority: self.priority,
370 },
371 ))
372 }
373}
374
375enum MempoolMessage {
376 Transaction(RawTransaction),
377 NewBlockMined,
378}
379
380struct MempoolShutdownGuard(Arc<AtomicBool>);
383
384impl Drop for MempoolShutdownGuard {
385 fn drop(&mut self) {
386 self.0.store(true, atomic::Ordering::Release);
387 }
388}
389
390const SHUTDOWN_POLL_INTERVAL: Duration = Duration::from_millis(50);
392
393async fn unless_shutdown<F>(
396 sync_mode: &AtomicU8,
397 request: F,
398) -> Result<Option<F::Output>, SyncModeError>
399where
400 F: Future,
401{
402 let mut interval = tokio::time::interval(SHUTDOWN_POLL_INTERVAL);
403 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
404 tokio::pin!(request);
405 loop {
406 tokio::select! {
407 output = &mut request => return Ok(Some(output)),
408 _ = interval.tick() => {
409 if SyncMode::from_atomic_u8(sync_mode)? == SyncMode::Shutdown {
410 return Ok(None);
411 }
412 }
413 }
414 }
415}
416
417pub async fn sync<C, P, W>(
432 client: C,
433 consensus_parameters: &P,
434 wallet: Arc<RwLock<W>>,
435 sync_mode: Arc<AtomicU8>,
436 progress: watch::Sender<Option<SyncStatus>>,
437 config: SyncConfig,
438) -> Result<SyncResult, SyncError<W::Error>>
439where
440 C: Clone + Indexer + TransparentIndexer + Sync + Send + 'static,
441 P: consensus::Parameters + Sync + Send + 'static,
442 W: SyncWallet
443 + SyncBlocks
444 + SyncTransactions
445 + SyncNullifiers
446 + SyncOutPoints
447 + SyncShardTrees
448 + Send,
449{
450 let mut sync_mode_enum = SyncMode::from_atomic_u8(&sync_mode)?;
451 if sync_mode_enum == SyncMode::NotRunning {
452 sync_mode_enum = SyncMode::Running;
453 sync_mode.store(sync_mode_enum as u8, atomic::Ordering::Release);
454 } else {
455 return Err(SyncModeError::SyncAlreadyRunning.into());
456 }
457
458 tracing::info!("Starting sync...");
459
460 let mut client_clone = client.clone();
463 client::check_lightwallet_protocol_version(&mut client_clone).await?;
464
465 let (fetch_request_sender, fetch_request_receiver) = mpsc::unbounded_channel();
467 let fetcher_handle =
468 tokio::spawn(
469 async move { client::fetch::fetch(fetch_request_receiver, client_clone).await },
470 );
471
472 let (mempool_transaction_sender, mut mempool_transaction_receiver) = mpsc::channel(100);
474 let shutdown_mempool = Arc::new(AtomicBool::new(false));
475 let _mempool_shutdown_guard = MempoolShutdownGuard(shutdown_mempool.clone());
476 let shutdown_mempool_clone = shutdown_mempool.clone();
477 let unprocessed_mempool_transactions_count = Arc::new(AtomicU32::new(0));
478 let unprocessed_mempool_transactions_count_clone =
479 unprocessed_mempool_transactions_count.clone();
480 let mempool_stream_connected_at = Arc::new(OnceLock::new());
481 let mempool_stream_connected_at_clone = mempool_stream_connected_at.clone();
482 let mempool_handle = tokio::spawn(async move {
483 mempool_monitor(
484 client,
485 mempool_transaction_sender,
486 unprocessed_mempool_transactions_count_clone,
487 mempool_stream_connected_at_clone,
488 shutdown_mempool_clone,
489 )
490 .await
491 });
492
493 let ufvks = wallet
494 .read()
495 .await
496 .get_unified_full_viewing_keys()
497 .map_err(SyncError::WalletError)?;
498 let (scan_results_sender, mut scan_results_receiver) = mpsc::unbounded_channel();
499 let mut scanner = Scanner::new(
500 consensus_parameters.clone(),
501 scan_results_sender,
502 fetch_request_sender.clone(),
503 ufvks.clone(),
504 config.transparent_address_discovery.gap_limit as u32,
505 );
506 scanner.launch(config.performance_level);
507
508 let mut wallet_guard = wallet.write().await;
509 state::reset_scan_ranges(
510 wallet_guard
511 .get_sync_state_mut()
512 .map_err(SyncError::WalletError)?,
513 );
514 rollback_unscanned_tree_states(&mut *wallet_guard)?;
515 drop(wallet_guard);
516
517 let mut check_for_new_blocks = false;
518 let mut first_verification_complete = false;
519 let mut mempool_shutdown_timer = None;
520 let mut nullifier_map_limit_exceeded = false;
521 let mut continuous_sync_interval =
522 tokio::time::interval(Duration::from_secs(CHECK_NEW_BLOCKS_INTERVAL));
523 continuous_sync_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
524 let mut interval = tokio::time::interval(Duration::from_millis(50));
525 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
526 'continuous_sync: loop {
527 continuous_sync_interval.reset();
528 let mut reorg_occured = false;
529 scanner.state.reverify();
530 let Some(chain_height) = unless_shutdown(
533 &sync_mode,
534 client::get_chain_height(fetch_request_sender.clone()),
535 )
536 .await?
537 else {
538 break 'continuous_sync;
539 };
540 let chain_height = chain_height?;
541 if chain_height == 0.into() {
542 return Err(SyncError::ServerError(ServerError::GenesisBlockOnly));
543 }
544
545 let mut wallet_guard = wallet.write().await;
546 let last_known_chain_height =
547 checked_wallet_height(&mut *wallet_guard, chain_height, consensus_parameters)?;
548 let new_blocks_mined = chain_height > last_known_chain_height;
549 state::create_scan_range(
550 last_known_chain_height,
551 chain_height,
552 wallet_guard
553 .get_sync_state_mut()
554 .map_err(SyncError::WalletError)?,
555 );
556 let tree_bounds_source = (!first_verification_complete || new_blocks_mined)
561 .then(|| state::TreeBoundsSource::read(&*wallet_guard, chain_height))
562 .transpose()
563 .map_err(SyncError::WalletError)?;
564 drop(wallet_guard);
565 if let Some(tree_bounds_source) = tree_bounds_source {
566 let Some(tree_bounds) = unless_shutdown(
567 &sync_mode,
568 state::fetch_tree_bounds(
569 consensus_parameters,
570 fetch_request_sender.clone(),
571 tree_bounds_source,
572 ),
573 )
574 .await?
575 else {
576 break 'continuous_sync;
577 };
578 let tree_bounds = tree_bounds?;
579 let mut wallet_guard = wallet.write().await;
580 if first_verification_complete {
581 state::update_wallet_tree_bounds(&mut *wallet_guard, tree_bounds)?;
582 } else {
583 state::set_initial_state(&mut *wallet_guard, chain_height, tree_bounds)?;
584 }
585 }
586
587 let mut initial_reorg_detection_start_height_opt =
590 if first_verification_complete && new_blocks_mined {
591 Some(last_known_chain_height + 1)
592 } else if first_verification_complete {
593 None
594 } else {
595 wallet
596 .read()
597 .await
598 .get_sync_state()
599 .map_err(SyncError::WalletError)?
600 .highest_scanned_height()
601 .map(|highest_scanned_height| highest_scanned_height + 1)
602 };
603
604 if let Some(initial_reorg_detection_start_height) = initial_reorg_detection_start_height_opt
605 {
606 if initial_reorg_detection_start_height <= chain_height {
607 state::set_verify_scan_range(
608 wallet
609 .write()
610 .await
611 .get_sync_state_mut()
612 .map_err(SyncError::WalletError)?,
613 initial_reorg_detection_start_height,
614 VerifyEnd::VerifyLowest,
615 );
616 } else {
617 let Some(chain_height_server_block) = unless_shutdown(
619 &sync_mode,
620 client::get_compact_block(fetch_request_sender.clone(), chain_height),
621 )
622 .await?
623 else {
624 break 'continuous_sync;
625 };
626 let chain_height_server_block = chain_height_server_block?;
627 let chain_height_wallet_block = wallet
628 .read()
629 .await
630 .get_wallet_block(chain_height)
631 .map_err(SyncError::WalletError)?;
632 if chain_height_wallet_block.block_hash().0.to_vec()
633 != chain_height_server_block.hash
634 {
635 tracing::info!("Re-org detected.");
636 reorg_occured = true;
637 let mut wallet_guard = wallet.write().await;
638 let scan_range_to_verify = state::set_verify_scan_range(
639 wallet_guard
640 .get_sync_state_mut()
641 .map_err(SyncError::WalletError)?,
642 chain_height,
643 VerifyEnd::VerifyHighest,
644 );
645 initial_reorg_detection_start_height_opt =
646 Some(scan_range_to_verify.block_range().start);
647 truncate_wallet_data(
648 &mut *wallet_guard,
649 initial_reorg_detection_start_height_opt
650 .expect("value must exist in this scope")
651 - 1,
652 )?;
653 let tree_bounds_source =
656 state::TreeBoundsSource::read(&*wallet_guard, chain_height)
657 .map_err(SyncError::WalletError)?;
658 drop(wallet_guard);
659 let Some(tree_bounds) = unless_shutdown(
660 &sync_mode,
661 state::fetch_tree_bounds(
662 consensus_parameters,
663 fetch_request_sender.clone(),
664 tree_bounds_source,
665 ),
666 )
667 .await?
668 else {
669 break 'continuous_sync;
670 };
671 state::set_initial_state(
672 &mut *wallet.write().await,
673 chain_height,
674 tree_bounds?,
675 )?;
676 } else {
677 scanner.state.verified();
679 }
680 }
681 } else {
682 scanner.state.verified();
685 }
686
687 if !first_verification_complete {
688 scanner.transparent_gap_addresses.extend(
697 transparent::address_discovery(
698 consensus_parameters,
699 wallet.clone(),
700 fetch_request_sender.clone(),
701 &ufvks,
702 last_known_chain_height,
703 chain_height,
704 config.transparent_address_discovery.clone(),
705 )
706 .await?,
707 );
708
709 let mut wallet_guard = wallet.write().await;
712 wallet_guard
713 .get_sync_state_mut()
714 .map_err(SyncError::WalletError)?
715 .transparent_scan_floor = Some(chain_height);
716 wallet_guard
717 .set_save_flag()
718 .map_err(SyncError::WalletError)?;
719 drop(wallet_guard);
720 }
721
722 if new_blocks_mined || reorg_occured || !first_verification_complete {
723 let fetch_start = subtree_root_fetch_start(&*wallet.read().await)?;
730 let Some(subtree_roots) = unless_shutdown(
731 &sync_mode,
732 fetch_subtree_roots(fetch_request_sender.clone(), fetch_start),
733 )
734 .await?
735 else {
736 break 'continuous_sync;
737 };
738 let subtree_roots = subtree_roots?;
739 add_fetched_subtree_roots(
740 consensus_parameters,
741 &mut *wallet.write().await,
742 subtree_roots,
743 )?;
744
745 if !first_verification_complete {
746 let initial_frontier_height =
750 initial_frontier_height(consensus_parameters, &*wallet.read().await)?;
751 if let Some(birthday) = initial_frontier_height {
752 let Some(frontiers) = unless_shutdown(
753 &sync_mode,
754 client::get_frontiers(
755 fetch_request_sender.clone(),
756 consensus_parameters,
757 birthday,
758 ),
759 )
760 .await?
761 else {
762 break 'continuous_sync;
763 };
764 let frontiers = frontiers?;
765 insert_initial_frontier(&mut *wallet.write().await, birthday, &frontiers)?;
766 }
767 }
768
769 state::prioritize_scan_ranges(
772 consensus_parameters,
773 chain_height,
774 &mut *wallet.write().await,
775 )
776 .map_err(SyncError::WalletError)?;
777 }
778
779 if new_blocks_mined || reorg_occured {
780 expire_transactions(&mut *wallet.write().await)?;
781
782 repin_anchor_checkpoints(consensus_parameters, &mut *wallet.write().await)?;
783 }
784
785 publish_sync_status(&*wallet.read().await, &progress).await?;
787
788 'scan: loop {
789 tokio::select! {
790 Some((load, scan_results)) = scan_results_receiver.recv() => {
791 let mut wallet_guard = wallet.write().await;
792 let ProcessedScanResults {
793 new_transparent_inuse_addresses,
794 new_transparent_gap_addresses,
795 reorg_detection_start_height,
796 } = process_scan_results(
797 consensus_parameters,
798 &mut *wallet_guard,
799 fetch_request_sender.clone(),
800 &ufvks,
801 load,
802 scanner.in_flight_tasks(),
803 scan_results,
804 initial_reorg_detection_start_height_opt,
805 config.performance_level,
806 &mut nullifier_map_limit_exceeded,
807 )
808 .await?;
809 scanner.retire_finished_tasks(
810 wallet_guard
811 .get_sync_state()
812 .map_err(SyncError::WalletError)?
813 .scan_ranges(),
814 );
815 scanner.update_transparent_gap_addresses(
824 &new_transparent_inuse_addresses,
825 new_transparent_gap_addresses,
826 );
827 if let Some(reorg_detection_start_height) = reorg_detection_start_height {
828 scanner.state.reverify();
831 initial_reorg_detection_start_height_opt = Some(
832 initial_reorg_detection_start_height_opt
833 .map_or(reorg_detection_start_height, |height| {
834 height.min(reorg_detection_start_height)
835 }),
836 );
837 }
838 expire_transactions(&mut *wallet_guard)?;
839 publish_sync_status(&*wallet_guard, &progress).await?;
840 wallet_guard.set_save_flag().map_err(SyncError::WalletError)?;
841 drop(wallet_guard);
842 }
843
844 Some(mempool_message) = mempool_transaction_receiver.recv() => {
845 match mempool_message {
846 MempoolMessage::Transaction(raw_transaction) => {
847 let mut wallet_guard = wallet.write().await;
848 process_mempool_transaction(
849 consensus_parameters,
850 &ufvks,
851 &mut *wallet_guard,
852 raw_transaction,
853 )
854 .await?;
855 unprocessed_mempool_transactions_count.fetch_sub(1, atomic::Ordering::Release);
856 wallet_guard.set_save_flag().map_err(SyncError::WalletError)?;
857 drop(wallet_guard);
858 }
859 MempoolMessage::NewBlockMined => {
860 check_for_new_blocks = true;
861 }
862 }
863 }
864
865 _update_scanner = interval.tick() => {
866 sync_mode_enum = SyncMode::from_atomic_u8(&sync_mode)?;
867 match sync_mode_enum {
868 SyncMode::Paused => {
869 let mut pause_interval = tokio::time::interval(Duration::from_secs(1));
870 pause_interval.tick().await;
871 while sync_mode_enum == SyncMode::Paused {
872 pause_interval.tick().await;
873 sync_mode_enum = SyncMode::from_atomic_u8(&sync_mode)?;
874 }
875 },
876 SyncMode::Shutdown => {
877 match mempool_drain_verdict(
878 shutdown_mempool.clone(),
879 unprocessed_mempool_transactions_count.clone(), mempool_shutdown_timer.get_or_insert_with(Instant::now).elapsed(),
880 mempool_stream_connected_at.get().map(|at| at.elapsed())
881 ).await {
882 MempoolDrainVerdict::NotShutdown | MempoolDrainVerdict::ShutdownNotDrained => {
883 continue 'scan;
884 }
885 MempoolDrainVerdict::ShutdownAndDrainComplete => {
886 break 'continuous_sync;
887 }
888 }
889 }
890 SyncMode::Running => (),
891 SyncMode::NotRunning => {
892 panic!("sync mode should not be manually set to NotRunning!");
893 },
894 }
895
896 if check_for_new_blocks && scanner.is_verified() {
897 check_for_new_blocks = false;
898 first_verification_complete = true;
899 continue 'continuous_sync;
900 }
901
902 scanner.update(&mut *wallet.write().await, nullifier_map_limit_exceeded).await?;
903
904 if matches!(scanner.state, ScannerState::Complete) && config.shutdown_on_completion {
905 let _ignore_mode_before_completion = SyncMode::apply(&sync_mode, SyncMode::on_completion)?;
906 }
907 }
908
909 _check_new_block_mined = continuous_sync_interval.tick() => {
910 check_for_new_blocks = true;
912 }
913 }
914 }
915 }
916
917 expire_transactions(&mut *wallet.write().await)?;
918
919 while scanner.worker_poolsize() != 0 {
921 let worker_id = scanner
922 .workers
923 .first()
924 .expect("non empty in this scope!")
925 .id();
926 scanner.shutdown_worker(worker_id).await;
927 }
928 scanner.shutdown_loader().await?;
929
930 let mut wallet_guard = wallet.write().await;
931 wallet_guard
932 .set_save_flag()
933 .map_err(SyncError::WalletError)?;
934 let sync_status = match sync_status(&*wallet_guard).await {
935 Ok(status) => status,
936 Err(SyncStatusError::WalletError(e)) => {
937 return Err(SyncError::WalletError(e));
938 }
939 Err(SyncStatusError::NoSyncData) => {
940 panic!("sync data must exist!");
941 }
942 Err(SyncStatusError::SyncProgressChannelClosed) => {
943 panic!("unreachable. outside of the context of progress channel.");
944 }
945 };
946 let _ignore_error = progress.send(Some(sync_status.clone()));
948
949 drop(wallet_guard);
950 drop(scanner);
951 drop(fetch_request_sender);
952
953 shutdown_mempool.store(true, atomic::Ordering::Release);
955 match mempool_handle.await.expect("task panicked") {
956 Ok(()) => (),
957 Err(e @ MempoolError::ShutdownWithoutStream) => tracing::warn!("{e}"),
958 Err(e) => return Err(e.into()),
959 }
960 fetcher_handle.abort();
963 if let Err(join_error) = fetcher_handle.await
964 && join_error.is_panic()
965 {
966 std::panic::resume_unwind(join_error.into_panic());
967 }
968 tracing::info!("Sync successfully shutdown.");
969
970 let sync_end_height = sync_status
972 .scan_ranges
973 .last()
974 .map_or(sync_status.sync_start_height, |scan_range| {
975 scan_range.block_range().end - 1
976 });
977
978 Ok(SyncResult {
979 sync_start_height: sync_status.sync_start_height,
980 sync_end_height,
981 blocks_scanned: sync_status.session_blocks_scanned,
982 sapling_outputs_scanned: sync_status.session_sapling_outputs_scanned,
983 orchard_outputs_scanned: sync_status.session_orchard_outputs_scanned,
984 ironwood_outputs_scanned: sync_status.session_ironwood_outputs_scanned,
985 total_outputs_scanned: sync_status.total_outputs_scanned,
986 total_outputs: sync_status.total_outputs,
987 nullifier_retrieval_pending: sync_status.nullifier_retrieval_pending(),
988 })
989}
990
991fn checked_wallet_height<W, P>(
1007 wallet: &mut W,
1008 chain_height: BlockHeight,
1009 consensus_parameters: &P,
1010) -> Result<BlockHeight, SyncError<W::Error>>
1011where
1012 W: SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
1013 P: zcash_protocol::consensus::Parameters,
1014{
1015 let sync_state = wallet.get_sync_state().map_err(SyncError::WalletError)?;
1016 if let Some(last_known_chain_height) = sync_state.last_known_chain_height() {
1017 if last_known_chain_height > chain_height {
1018 if last_known_chain_height - chain_height >= MAX_REORG_ALLOWANCE {
1019 return Err(SyncError::ChainError(
1023 u32::from(last_known_chain_height),
1024 MAX_REORG_ALLOWANCE,
1025 u32::from(chain_height),
1026 ));
1027 }
1028 truncate_wallet_data(wallet, chain_height)?;
1031 let sync_state = wallet
1032 .get_sync_state_mut()
1033 .map_err(SyncError::WalletError)?;
1034 state::truncate_scan_ranges(chain_height, sync_state);
1035 state::lower_transparent_scan_floor(sync_state, chain_height);
1039 wallet.set_save_flag().map_err(SyncError::WalletError)?;
1040 return Ok(chain_height);
1041 }
1042 Ok(last_known_chain_height)
1044 } else {
1045 let sapling_activation_height = consensus_parameters
1047 .activation_height(consensus::NetworkUpgrade::Sapling)
1048 .expect("sapling activation height should always return Some");
1049 let birthday = wallet.get_birthday().map_err(SyncError::WalletError)?;
1050 if birthday > chain_height {
1051 return Err(SyncError::ChainError(
1054 u32::from(birthday),
1055 MAX_REORG_ALLOWANCE,
1056 u32::from(chain_height),
1057 ));
1058 } else if birthday < sapling_activation_height {
1059 return Err(SyncError::BirthdayBelowSapling(
1060 u32::from(birthday),
1061 u32::from(sapling_activation_height),
1062 ));
1063 }
1064
1065 Ok(birthday - 1)
1066 }
1067}
1068
1069pub async fn sync_status<W>(wallet: &W) -> Result<SyncStatus, SyncStatusError<W::Error>>
1073where
1074 W: SyncWallet + SyncBlocks,
1075{
1076 fn output_pool_total(sapling: u32, orchard: u32, ironwood: u32) -> u64 {
1082 u64::from(sapling) + u64::from(orchard) + u64::from(ironwood)
1083 }
1084
1085 const INCLUSIVE_SPAN_ADJUSTMENT: u32 = 1;
1087
1088 let (
1089 total_sapling_outputs_scanned,
1090 total_orchard_outputs_scanned,
1091 total_ironwood_outputs_scanned,
1092 ) = state::calculate_scanned_outputs(wallet).map_err(SyncStatusError::WalletError)?;
1093 let total_outputs_scanned = output_pool_total(
1094 total_sapling_outputs_scanned,
1095 total_orchard_outputs_scanned,
1096 total_ironwood_outputs_scanned,
1097 );
1098
1099 let sync_state = wallet
1100 .get_sync_state()
1101 .map_err(SyncStatusError::WalletError)?;
1102 if sync_state.initial_sync_state.sync_start_height == 0.into() {
1103 return Ok(SyncStatus {
1104 scan_ranges: sync_state.scan_ranges.clone(),
1105 sync_start_height: 0.into(),
1106 session_blocks_scanned: 0,
1107 total_blocks_scanned: 0,
1108 total_blocks: 0,
1109 session_sapling_outputs_scanned: 0,
1110 session_orchard_outputs_scanned: 0,
1111 session_ironwood_outputs_scanned: 0,
1112 total_sapling_outputs_scanned: 0,
1113 total_orchard_outputs_scanned: 0,
1114 total_ironwood_outputs_scanned: 0,
1115 total_outputs_scanned: 0,
1116 total_outputs: 0,
1117 });
1118 }
1119 let total_blocks_scanned = state::calculate_scanned_blocks(sync_state);
1120
1121 let birthday = sync_state
1122 .wallet_birthday()
1123 .ok_or(SyncStatusError::NoSyncData)?;
1124 let last_known_chain_height = sync_state
1125 .last_known_chain_height()
1126 .ok_or(SyncStatusError::NoSyncData)?;
1127 let total_blocks = u32::from(last_known_chain_height)
1128 .saturating_sub(u32::from(birthday))
1129 .saturating_add(INCLUSIVE_SPAN_ADJUSTMENT);
1130 let total_sapling_outputs = sync_state
1131 .initial_sync_state
1132 .wallet_tree_bounds
1133 .sapling_final_tree_size
1134 .saturating_sub(
1135 sync_state
1136 .initial_sync_state
1137 .wallet_tree_bounds
1138 .sapling_initial_tree_size,
1139 );
1140 let total_orchard_outputs = sync_state
1141 .initial_sync_state
1142 .wallet_tree_bounds
1143 .orchard_final_tree_size
1144 .saturating_sub(
1145 sync_state
1146 .initial_sync_state
1147 .wallet_tree_bounds
1148 .orchard_initial_tree_size,
1149 );
1150 let total_ironwood_outputs = sync_state
1151 .initial_sync_state
1152 .wallet_tree_bounds
1153 .ironwood_final_tree_size
1154 .saturating_sub(
1155 sync_state
1156 .initial_sync_state
1157 .wallet_tree_bounds
1158 .ironwood_initial_tree_size,
1159 );
1160 let total_outputs = output_pool_total(
1161 total_sapling_outputs,
1162 total_orchard_outputs,
1163 total_ironwood_outputs,
1164 );
1165
1166 let session_blocks_scanned = total_blocks_scanned
1167 .saturating_sub(sync_state.initial_sync_state.previously_scanned_blocks);
1168
1169 let session_sapling_outputs_scanned = total_sapling_outputs_scanned.saturating_sub(
1170 sync_state
1171 .initial_sync_state
1172 .previously_scanned_sapling_outputs,
1173 );
1174 let session_orchard_outputs_scanned = total_orchard_outputs_scanned.saturating_sub(
1175 sync_state
1176 .initial_sync_state
1177 .previously_scanned_orchard_outputs,
1178 );
1179 let session_ironwood_outputs_scanned = total_ironwood_outputs_scanned.saturating_sub(
1180 sync_state
1181 .initial_sync_state
1182 .previously_scanned_ironwood_outputs,
1183 );
1184 Ok(SyncStatus {
1185 scan_ranges: sync_state.scan_ranges.clone(),
1186 sync_start_height: sync_state.initial_sync_state.sync_start_height,
1187 session_blocks_scanned,
1188 total_blocks_scanned,
1189 total_blocks,
1190 session_sapling_outputs_scanned,
1191 total_sapling_outputs_scanned,
1192 session_orchard_outputs_scanned,
1193 total_orchard_outputs_scanned,
1194 session_ironwood_outputs_scanned,
1195 total_ironwood_outputs_scanned,
1196 total_outputs_scanned,
1197 total_outputs,
1198 })
1199}
1200
1201async fn publish_sync_status<W>(
1203 wallet: &W,
1204 progress: &watch::Sender<Option<SyncStatus>>,
1205) -> Result<(), SyncStatusError<W::Error>>
1206where
1207 W: SyncWallet + SyncBlocks,
1208{
1209 match sync_status(wallet).await {
1210 Ok(status) => {
1211 progress
1212 .send(Some(status))
1213 .map_err(|_| SyncStatusError::SyncProgressChannelClosed)?;
1214 }
1215 Err(e) => {
1216 return Err(e);
1217 }
1218 }
1219
1220 Ok(())
1221}
1222
1223pub fn scan_pending_transaction<W>(
1230 consensus_parameters: &impl consensus::Parameters,
1231 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
1232 wallet: &mut W,
1233 transaction: Transaction,
1234 status: ConfirmationStatus,
1235 datetime: u32,
1236) -> Result<(), SyncError<W::Error>>
1237where
1238 W: SyncWallet + SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
1239{
1240 if matches!(status, ConfirmationStatus::Confirmed(_)) {
1241 panic!("this fn is for unconfirmed transactions only");
1242 }
1243
1244 let mut pending_transaction_nullifiers = NullifierMap::new();
1245 let mut pending_transaction_outpoints = BTreeMap::new();
1246 let transparent_addresses: HashMap<String, TransparentAddressId> = wallet
1247 .get_transparent_addresses()
1248 .map_err(SyncError::WalletError)?
1249 .iter()
1250 .map(|(id, address)| (address.clone(), *id))
1251 .collect();
1252 let pending_transaction = scan_transaction(
1253 consensus_parameters,
1254 ufvks,
1255 transaction.txid(),
1256 transaction,
1257 status,
1258 None,
1259 &mut pending_transaction_nullifiers,
1260 &mut pending_transaction_outpoints,
1261 &transparent_addresses,
1262 datetime,
1263 )?;
1264
1265 let wallet_transactions = wallet
1266 .get_wallet_transactions()
1267 .map_err(SyncError::WalletError)?;
1268 let transparent_output_ids =
1269 spend::collect_transparent_output_ids(wallet_transactions.values());
1270 let transparent_spend_scan_targets =
1271 spend::detect_spends(&pending_transaction_outpoints, &transparent_output_ids);
1272 let (sapling_derived_nullifiers, orchard_derived_nullifiers, ironwood_derived_nullifiers) =
1273 spend::collect_derived_nullifiers(wallet_transactions.values());
1274 let shielded_spend_scan_targets = spend::detect_shielded_spends(
1275 &pending_transaction_nullifiers,
1276 &sapling_derived_nullifiers,
1277 &orchard_derived_nullifiers,
1278 &ironwood_derived_nullifiers,
1279 );
1280
1281 if pending_transaction.transparent_coins().is_empty()
1283 && pending_transaction.sapling_notes().is_empty()
1284 && pending_transaction.orchard_notes().is_empty()
1285 && pending_transaction.ironwood_notes().is_empty()
1286 && pending_transaction.outgoing_sapling_notes().is_empty()
1287 && pending_transaction.outgoing_orchard_notes().is_empty()
1288 && pending_transaction.outgoing_ironwood_notes().is_empty()
1289 && transparent_spend_scan_targets.is_empty()
1290 && shielded_spend_scan_targets.is_empty()
1291 {
1292 return Ok(());
1293 }
1294
1295 wallet
1296 .insert_wallet_transaction(pending_transaction)
1297 .map_err(SyncError::WalletError)?;
1298 spend::update_spent_coins(
1299 wallet
1300 .get_wallet_transactions_mut()
1301 .map_err(SyncError::WalletError)?,
1302 transparent_spend_scan_targets,
1303 );
1304 spend::update_spent_notes(wallet, shielded_spend_scan_targets, false)
1305 .map_err(SyncError::WalletError)?;
1306
1307 Ok(())
1308}
1309
1310pub fn add_scan_targets(sync_state: &mut SyncState, scan_targets: &[ScanTarget]) {
1320 for scan_target in scan_targets {
1321 sync_state.insert_scan_target(*scan_target);
1322 }
1323}
1324
1325pub fn reset_spends(
1331 wallet_transactions: &mut HashMap<TxId, WalletTransaction>,
1332 invalid_txids: Vec<TxId>,
1333) {
1334 wallet_transactions
1335 .values_mut()
1336 .flat_map(|transaction| transaction.ironwood_notes_mut())
1337 .filter(|output| {
1338 output
1339 .spending_transaction
1340 .is_some_and(|spending_txid| invalid_txids.contains(&spending_txid))
1341 })
1342 .for_each(|output| {
1343 output.set_spending_transaction(None);
1344 });
1345 wallet_transactions
1346 .values_mut()
1347 .flat_map(|transaction| transaction.orchard_notes_mut())
1348 .filter(|output| {
1349 output
1350 .spending_transaction
1351 .is_some_and(|spending_txid| invalid_txids.contains(&spending_txid))
1352 })
1353 .for_each(|output| {
1354 output.set_spending_transaction(None);
1355 });
1356 wallet_transactions
1357 .values_mut()
1358 .flat_map(|transaction| transaction.sapling_notes_mut())
1359 .filter(|output| {
1360 output
1361 .spending_transaction
1362 .is_some_and(|spending_txid| invalid_txids.contains(&spending_txid))
1363 })
1364 .for_each(|output| {
1365 output.set_spending_transaction(None);
1366 });
1367 wallet_transactions
1368 .values_mut()
1369 .flat_map(|transaction| transaction.transparent_coins_mut())
1370 .filter(|output| {
1371 output
1372 .spending_transaction
1373 .is_some_and(|spending_txid| invalid_txids.contains(&spending_txid))
1374 })
1375 .for_each(|output| {
1376 output.set_spending_transaction(None);
1377 });
1378}
1379
1380pub fn set_transactions_failed(
1392 wallet_transactions: &mut HashMap<TxId, WalletTransaction>,
1393 failed_txids: Vec<TxId>,
1394) {
1395 let (confirmed_txids, failable_txids): (Vec<TxId>, Vec<TxId>) =
1396 failed_txids.into_iter().partition(|txid| {
1397 wallet_transactions
1398 .get(txid)
1399 .is_some_and(|transaction| transaction.status().is_confirmed())
1400 });
1401 for confirmed_txid in confirmed_txids {
1402 tracing::warn!(
1403 "refusing to fail transaction {confirmed_txid} with `Confirmed` status! \
1404 a mined transaction can only be invalidated by truncation."
1405 );
1406 }
1407 set_transactions_failed_unchecked(wallet_transactions, failable_txids);
1408}
1409
1410pub(crate) fn set_transactions_failed_unchecked(
1417 wallet_transactions: &mut HashMap<TxId, WalletTransaction>,
1418 failed_txids: Vec<TxId>,
1419) {
1420 for failed_txid in failed_txids.iter() {
1421 if let Some(transaction) = wallet_transactions.get_mut(failed_txid) {
1422 let height = transaction.status().get_height();
1423 transaction.update_status(
1424 ConfirmationStatus::Failed(height),
1425 SystemTime::now()
1426 .duration_since(SystemTime::UNIX_EPOCH)
1427 .expect("infalliable for such long time periods")
1428 .as_secs() as u32,
1429 true,
1430 );
1431 }
1432 }
1433 reset_spends(wallet_transactions, failed_txids);
1434}
1435
1436#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1437enum MempoolDrainVerdict {
1438 ShutdownAndDrainComplete,
1441 ShutdownNotDrained,
1444 NotShutdown,
1446}
1447
1448async fn mempool_drain_verdict(
1449 shutdown_mempool: Arc<AtomicBool>,
1450 unprocessed_mempool_transactions_count: Arc<AtomicU32>,
1451 mempool_shutdown_timer: Duration,
1452 mempool_stream_connection_timer: Option<Duration>,
1453) -> MempoolDrainVerdict {
1454 use zingo_netutils::time::{MEMPOOL_DRAIN_CEILING, MEMPOOL_DRAIN_SETTLE};
1455
1456 if mempool_shutdown_timer < MEMPOOL_DRAIN_CEILING {
1457 let Some(mempool_elapsed) = mempool_stream_connection_timer else {
1458 return MempoolDrainVerdict::NotShutdown;
1460 };
1461 if let Some(mempool_startup_remaining) = MEMPOOL_DRAIN_SETTLE.checked_sub(mempool_elapsed) {
1463 tokio::time::sleep(mempool_startup_remaining).await;
1464 }
1465 }
1466
1467 shutdown_mempool.store(true, atomic::Ordering::Release);
1469
1470 if unprocessed_mempool_transactions_count.load(atomic::Ordering::Acquire) > 0 {
1472 return MempoolDrainVerdict::ShutdownNotDrained;
1473 }
1474
1475 MempoolDrainVerdict::ShutdownAndDrainComplete
1476}
1477
1478#[derive(Debug, Default)]
1480struct ProcessedScanResults {
1481 new_transparent_inuse_addresses: HashMap<String, TransparentAddressId>,
1483 new_transparent_gap_addresses: HashMap<String, TransparentAddressId>,
1485 reorg_detection_start_height: Option<BlockHeight>,
1489}
1490
1491#[allow(clippy::too_many_arguments)]
1495async fn process_scan_results<W>(
1496 consensus_parameters: &(impl consensus::Parameters + Sync),
1497 wallet: &mut W,
1498 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
1499 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
1500 load: ScanLoad,
1501 in_flight_tasks: &BTreeMap<TaskId, ScanRange>,
1502 scan_results: Result<ScanResults, ScanError>,
1503 initial_reorg_detection_start_height: Option<BlockHeight>,
1504 performance_level: PerformanceLevel,
1505 nullifier_map_limit_exceeded: &mut bool,
1506) -> Result<ProcessedScanResults, SyncError<W::Error>>
1507where
1508 W: SyncWallet
1509 + SyncBlocks
1510 + SyncTransactions
1511 + SyncNullifiers
1512 + SyncOutPoints
1513 + SyncShardTrees
1514 + Send,
1515{
1516 let ScanLoad {
1517 task_id,
1518 scan_range,
1519 } = load;
1520 let task = in_flight_tasks
1521 .get(&task_id)
1522 .expect("a task stays in flight until its last load is processed");
1523 let later_tasks = in_flight_tasks
1524 .range((Bound::Excluded(task_id), Bound::Unbounded))
1525 .map(|(_, task)| task.block_range().clone())
1526 .collect::<Vec<_>>();
1527 let sync_state = wallet
1528 .get_sync_state_mut()
1529 .map_err(SyncError::WalletError)?;
1530 match state::scan_results_standing(
1531 sync_state.scan_ranges(),
1532 &later_tasks,
1533 task,
1534 scan_range.block_range(),
1535 ) {
1536 ScanResultsStanding::Current => {}
1537 ScanResultsStanding::Stale { cause, reset } => {
1538 state::reset_stale_load(sync_state, task.priority(), &reset);
1539 tracing::info!("Stale scan results of {scan_range} discarded: {cause:?}.");
1540
1541 return Ok(ProcessedScanResults::default());
1542 }
1543 }
1544
1545 match scan_results {
1546 Ok(results) => {
1547 let ScanResults {
1548 mut nullifiers,
1549 mut outpoints,
1550 scanned_blocks,
1551 mut wallet_transactions,
1552 dropped_scan_targets,
1553 sapling_located_trees,
1554 orchard_located_trees,
1555 ironwood_located_trees,
1556 new_transparent_inuse_addresses,
1557 new_transparent_gap_addresses,
1558 } = results;
1559
1560 wallet
1561 .get_sync_state_mut()
1562 .map_err(SyncError::WalletError)?
1563 .scan_targets
1564 .retain(|scan_target| !dropped_scan_targets.contains(scan_target));
1565
1566 if scan_range.priority() == ScanPriority::ScannedWithoutMapping {
1567 let first_unscanned_range = wallet
1568 .get_sync_state()
1569 .map_err(SyncError::WalletError)?
1570 .scan_ranges
1571 .iter()
1572 .find(|scan_range| scan_range.priority() != ScanPriority::Scanned)
1573 .expect("the scan range being processed is not yet set to scanned so at least one unscanned range must exist");
1574 if !first_unscanned_range.encloses(scan_range.block_range()) {
1575 state::reset_refetching_nullifiers_scan_range(
1579 wallet
1580 .get_sync_state_mut()
1581 .map_err(SyncError::WalletError)?,
1582 scan_range.block_range().clone(),
1583 );
1584 tracing::debug!(
1585 "Nullifiers discarded and will be re-fetched to avoid missing spends."
1586 );
1587
1588 return Ok(ProcessedScanResults {
1589 new_transparent_inuse_addresses,
1590 new_transparent_gap_addresses,
1591 reorg_detection_start_height: None,
1592 });
1593 }
1594
1595 let mut missing_block_bounds = BTreeMap::new();
1601 let full_refetching_nullifiers_range = wallet
1602 .get_sync_state()
1603 .map_err(SyncError::WalletError)?
1604 .scan_ranges
1605 .iter()
1606 .find(|&wallet_scan_range| wallet_scan_range.encloses(scan_range.block_range()))
1607 .expect("wallet scan range containing scan range should exist!");
1608 if scan_range.block_range().start
1609 != full_refetching_nullifiers_range.block_range().start
1610 || scan_range.block_range().end
1611 != full_refetching_nullifiers_range.block_range().end
1612 {
1613 for block_bound in [
1614 scan_range.block_range().start - 1,
1615 scan_range.block_range().start,
1616 scan_range.block_range().end - 1,
1617 scan_range.block_range().end,
1618 ] {
1619 if block_bound < full_refetching_nullifiers_range.block_range().start
1620 || block_bound >= full_refetching_nullifiers_range.block_range().end
1621 {
1622 continue;
1623 }
1624 if wallet.get_wallet_block(block_bound).is_err() {
1625 missing_block_bounds.insert(
1626 block_bound,
1627 WalletBlock::from_compact_block(
1628 consensus_parameters,
1629 fetch_request_sender.clone(),
1630 &client::get_compact_block(
1631 fetch_request_sender.clone(),
1632 block_bound,
1633 )
1634 .await?,
1635 )
1636 .await?,
1637 );
1638 }
1639 }
1640 }
1641 let shielded_spend_scan_targets = spend::locate_shielded_spends(
1642 consensus_parameters,
1643 &*wallet,
1644 fetch_request_sender.clone(),
1645 ufvks,
1646 &missing_block_bounds,
1647 &mut wallet_transactions,
1648 &nullifiers,
1649 )
1650 .await?;
1651
1652 if !missing_block_bounds.is_empty() {
1653 wallet
1654 .append_wallet_blocks(missing_block_bounds)
1655 .map_err(SyncError::WalletError)?;
1656 }
1657 wallet
1658 .extend_wallet_transactions(wallet_transactions)
1659 .map_err(SyncError::WalletError)?;
1660 spend::apply_shielded_spends(
1661 consensus_parameters,
1662 wallet,
1663 shielded_spend_scan_targets,
1664 )
1665 .map_err(SyncError::WalletError)?;
1666
1667 state::set_scanned_scan_range(
1668 wallet
1669 .get_sync_state_mut()
1670 .map_err(SyncError::WalletError)?,
1671 scan_range.block_range().clone(),
1672 true, );
1674 } else {
1675 if !*nullifier_map_limit_exceeded {
1677 let nullifier_map = wallet.get_nullifiers().map_err(SyncError::WalletError)?;
1678 if max_nullifier_map_size(performance_level).is_some_and(|max| {
1679 nullifier_map.orchard.len()
1680 + nullifier_map.sapling.len()
1681 + nullifier_map.ironwood.len()
1682 + nullifiers.orchard.len()
1683 + nullifiers.sapling.len()
1684 + nullifiers.ironwood.len()
1685 > max
1686 }) {
1687 *nullifier_map_limit_exceeded = true;
1688 }
1689 }
1690 let mut map_nullifiers = !*nullifier_map_limit_exceeded;
1691
1692 for query_scan_range in wallet
1699 .get_sync_state()
1700 .map_err(SyncError::WalletError)?
1701 .scan_ranges()
1702 {
1703 let scan_priority = query_scan_range.priority();
1704 if scan_priority != ScanPriority::Scanned
1705 && scan_priority != ScanPriority::Scanning
1706 && scan_priority != ScanPriority::RefetchingNullifiers
1707 {
1708 break;
1709 }
1710
1711 if scan_priority == ScanPriority::Scanning
1712 && query_scan_range.encloses(scan_range.block_range())
1713 {
1714 map_nullifiers = true;
1715 break;
1716 }
1717 }
1718
1719 let transparent_spend_scan_targets = spend::locate_transparent_spends(
1724 consensus_parameters,
1725 &*wallet,
1726 fetch_request_sender.clone(),
1727 ufvks,
1728 &scanned_blocks,
1729 &mut wallet_transactions,
1730 &outpoints,
1731 )
1732 .await?;
1733 let shielded_spend_scan_targets = spend::locate_shielded_spends(
1734 consensus_parameters,
1735 &*wallet,
1736 fetch_request_sender.clone(),
1737 ufvks,
1738 &scanned_blocks,
1739 &mut wallet_transactions,
1740 &nullifiers,
1741 )
1742 .await?;
1743
1744 update_wallet_data(
1745 consensus_parameters,
1746 wallet,
1747 fetch_request_sender,
1748 ufvks,
1749 &scan_range,
1750 if map_nullifiers {
1751 Some(&mut nullifiers)
1752 } else {
1753 None
1754 },
1755 &mut outpoints,
1758 wallet_transactions,
1759 sapling_located_trees,
1760 orchard_located_trees,
1761 ironwood_located_trees,
1762 &new_transparent_inuse_addresses,
1763 )
1764 .await?;
1765 spend::apply_transparent_spends(wallet, transparent_spend_scan_targets)
1766 .map_err(SyncError::WalletError)?;
1767 spend::apply_shielded_spends(
1768 consensus_parameters,
1769 wallet,
1770 shielded_spend_scan_targets,
1771 )
1772 .map_err(SyncError::WalletError)?;
1773 add_scanned_blocks(wallet, scanned_blocks, &scan_range)
1774 .map_err(SyncError::WalletError)?;
1775
1776 state::set_scanned_scan_range(
1777 wallet
1778 .get_sync_state_mut()
1779 .map_err(SyncError::WalletError)?,
1780 scan_range.block_range().clone(),
1781 map_nullifiers,
1782 );
1783 state::merge_scan_ranges(
1784 wallet
1785 .get_sync_state_mut()
1786 .map_err(SyncError::WalletError)?,
1787 ScanPriority::ScannedWithoutMapping,
1788 );
1789 }
1790
1791 state::merge_scan_ranges(
1792 wallet
1793 .get_sync_state_mut()
1794 .map_err(SyncError::WalletError)?,
1795 ScanPriority::Scanned,
1796 );
1797 remove_irrelevant_data(wallet).map_err(SyncError::WalletError)?;
1798 tracing::debug!("Scan results processed.");
1799
1800 Ok(ProcessedScanResults {
1801 new_transparent_inuse_addresses,
1802 new_transparent_gap_addresses,
1803 reorg_detection_start_height: None,
1804 })
1805 }
1806 Err(ScanError::ContinuityError(ContinuityError::HashDiscontinuity { height, .. })) => {
1807 tracing::warn!("Hash discontinuity detected before block {height}.");
1808 if height == scan_range.block_range().start
1809 && scan_range.priority() == ScanPriority::Verify
1810 {
1811 tracing::info!("Re-org detected.");
1812 let sync_state = wallet
1813 .get_sync_state_mut()
1814 .map_err(SyncError::WalletError)?;
1815 let last_known_chain_height = sync_state
1816 .last_known_chain_height()
1817 .expect("scan ranges should be non-empty in this scope");
1818
1819 state::reset_verify_scan_range(sync_state, scan_range.block_range());
1820
1821 let current_reorg_detection_start_height = state::set_verify_scan_range(
1823 sync_state,
1824 height - 1,
1825 state::VerifyEnd::VerifyHighest,
1826 )
1827 .block_range()
1828 .start;
1829 state::merge_scan_ranges(sync_state, ScanPriority::Verify);
1830
1831 if initial_reorg_detection_start_height
1832 .expect("re-org can only be detected in wallets that have synced previously!")
1833 - current_reorg_detection_start_height
1834 > MAX_REORG_ALLOWANCE
1835 {
1836 clear_wallet_data(wallet)?;
1837
1838 return Err(ServerError::ChainVerificationError.into());
1839 }
1840
1841 let reorg_truncate_height = current_reorg_detection_start_height - 1;
1842 truncate_wallet_data(wallet, reorg_truncate_height)?;
1843 let sync_state = wallet
1844 .get_sync_state_mut()
1845 .map_err(SyncError::WalletError)?;
1846 state::reopen_scan_ranges_inner(sync_state, current_reorg_detection_start_height);
1849 state::lower_transparent_scan_floor(sync_state, reorg_truncate_height);
1852
1853 let tree_bounds_source =
1857 state::TreeBoundsSource::read(&*wallet, last_known_chain_height)
1858 .map_err(SyncError::WalletError)?;
1859 let tree_bounds = state::fetch_tree_bounds(
1860 consensus_parameters,
1861 fetch_request_sender.clone(),
1862 tree_bounds_source,
1863 )
1864 .await?;
1865 state::set_initial_state(wallet, last_known_chain_height, tree_bounds)?;
1866
1867 Ok(ProcessedScanResults::default())
1868 } else if height == scan_range.block_range().start {
1869 tracing::info!("Verifying {scan_range} again.");
1874 let sync_state = wallet
1875 .get_sync_state_mut()
1876 .map_err(SyncError::WalletError)?;
1877 state::reset_stale_load(
1878 sync_state,
1879 task.priority(),
1880 std::slice::from_ref(scan_range.block_range()),
1881 );
1882 state::set_verify_scan_range(sync_state, height, state::VerifyEnd::VerifyLowest);
1883 state::merge_scan_ranges(sync_state, ScanPriority::Verify);
1884
1885 Ok(ProcessedScanResults {
1886 reorg_detection_start_height: Some(height),
1887 ..Default::default()
1888 })
1889 } else {
1890 Err(scan_results
1891 .expect_err("must be error variant in this scope")
1892 .into())
1893 }
1894 }
1895 Err(ScanError::IncorrectTreeSize {
1896 shielded_protocol: PoolType::Shielded(pool),
1897 height,
1898 block_metadata_size,
1899 calculated_size,
1900 }) => {
1901 tracing::error!(
1902 "RESCAN TRIGGERED: at height {height}, {pool:?} history recorded a commitment \
1903 tree of {calculated_size} where the chain reports {block_metadata_size}; the \
1904 wallet's {pool:?} records are being cleared back to the pool activation height \
1905 and the next sync rescans from there."
1906 );
1907 Err(truncate_to_pool_activation_height(
1908 consensus_parameters,
1909 fetch_request_sender.clone(),
1910 wallet,
1911 pool,
1912 height,
1913 block_metadata_size,
1914 calculated_size,
1915 )
1916 .await?)
1917 }
1918 Err(e) => Err(e.into()),
1919 }
1920}
1921
1922async fn truncate_to_pool_activation_height<W>(
1933 consensus_parameters: &impl consensus::Parameters,
1934 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
1935 wallet: &mut W,
1936 target_pool: ShieldedPool,
1937 disagreed_at: BlockHeight,
1938 block_metadata_size: u32,
1939 calculated_size: u32,
1940) -> Result<SyncError<W::Error>, SyncError<W::Error>>
1941where
1942 W: SyncWallet + SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
1943{
1944 let birthday = wallet.get_birthday().map_err(SyncError::WalletError)?;
1945 let Some(activation) = PoolActivation::of(consensus_parameters, target_pool) else {
1946 panic!("{target_pool:?} reported a tree size on a chain that never activates it");
1949 };
1950 let rescan_from = activation.max_with(birthday);
1951 let rescan_targets = wallet
1954 .get_wallet_transactions()
1955 .map_err(SyncError::WalletError)?
1956 .values()
1957 .filter(|&transaction| transaction.status().is_confirmed_after_or_at(&rescan_from))
1958 .map(|transaction| {
1959 ScanTarget::internal(transaction.status().get_height(), transaction.txid())
1960 })
1961 .collect::<Vec<_>>();
1962
1963 let frontiers =
1966 client::get_frontiers(fetch_request_sender.clone(), consensus_parameters, birthday).await?;
1967 let missing_block_bound = state::fetch_reopened_block_bound(
1968 consensus_parameters,
1969 fetch_request_sender,
1970 &*wallet,
1971 rescan_from,
1972 )
1973 .await?;
1974
1975 let shard_trees = wallet
1979 .get_shard_trees_mut()
1980 .map_err(SyncError::WalletError)?;
1981 for pool in [
1982 ShieldedPool::Sapling,
1983 ShieldedPool::Orchard,
1984 ShieldedPool::Ironwood,
1985 ] {
1986 if pool < target_pool {
1987 match pool {
1988 ShieldedPool::Sapling => {
1989 truncate_tree_to_next_checkpoint(rescan_from - 1, &mut shard_trees.sapling)?;
1990 }
1991 ShieldedPool::Orchard => {
1992 truncate_tree_to_next_checkpoint(rescan_from - 1, &mut shard_trees.orchard)?;
1993 }
1994 ShieldedPool::Ironwood => {
1995 truncate_tree_to_next_checkpoint(rescan_from - 1, &mut shard_trees.ironwood)?;
1996 }
1997 }
1998 }
1999 }
2000
2001 truncate_stores(wallet, rescan_from - 1, false)?;
2002
2003 let sync_state = wallet
2007 .get_sync_state_mut()
2008 .map_err(SyncError::WalletError)?;
2009 for pool in [
2010 ShieldedPool::Sapling,
2011 ShieldedPool::Orchard,
2012 ShieldedPool::Ironwood,
2013 ] {
2014 if pool >= target_pool {
2015 state::clear_shard_ranges(sync_state, pool);
2016 }
2017 }
2018
2019 let retention = Retention::Checkpoint {
2020 id: birthday,
2021 marking: Marking::None,
2022 };
2023 let shard_trees = wallet
2024 .get_shard_trees_mut()
2025 .map_err(SyncError::WalletError)?;
2026 for pool in [
2027 ShieldedPool::Sapling,
2028 ShieldedPool::Orchard,
2029 ShieldedPool::Ironwood,
2030 ] {
2031 if pool >= target_pool {
2032 shard_trees.clear_pool(pool);
2033 match pool {
2034 ShieldedPool::Sapling => shard_trees
2035 .sapling
2036 .insert_frontier(frontiers.final_sapling_tree().clone(), retention),
2037 ShieldedPool::Orchard => shard_trees
2038 .orchard
2039 .insert_frontier(frontiers.final_orchard_tree().clone(), retention),
2040 ShieldedPool::Ironwood => shard_trees
2041 .ironwood
2042 .insert_frontier(frontiers.final_ironwood_tree().clone(), retention),
2043 }
2044 .expect("infallible");
2045 }
2046 }
2047
2048 state::reopen_scan_ranges_from(wallet, rescan_from, missing_block_bound)
2049 .map_err(SyncError::WalletError)?;
2050 let sync_state = wallet
2051 .get_sync_state_mut()
2052 .map_err(SyncError::WalletError)?;
2053 for rescan_target in rescan_targets {
2054 sync_state.insert_scan_target(rescan_target);
2055 }
2056 wallet.set_save_flag().map_err(SyncError::WalletError)?;
2057
2058 Ok(SyncError::PoolHistoryReopened {
2059 pool: PoolType::Shielded(target_pool),
2060 rescan_from,
2061 disagreed_at,
2062 block_metadata_size,
2063 calculated_size,
2064 })
2065}
2066
2067fn truncate_tree_to_next_checkpoint<H, const DEPTH: u8, const SHARD_HEIGHT: u8>(
2072 target_height: BlockHeight,
2073 tree: &mut ShardTree<MemoryShardStore<H, BlockHeight>, DEPTH, SHARD_HEIGHT>,
2074) -> Result<(), shardtree::error::ShardTreeError<Infallible>>
2075where
2076 H: incrementalmerkletree::Hashable + Clone + PartialEq,
2077{
2078 let mut truncation_height = None;
2079 let checkpoint_count = tree.store().checkpoint_count().expect("infallible");
2080 tree.store()
2081 .for_each_checkpoint(checkpoint_count, |height, _| {
2082 if truncation_height.is_some() {
2083 return Ok(());
2084 }
2085
2086 if *height >= target_height {
2087 truncation_height = Some(*height);
2088 }
2089
2090 Ok(())
2091 })
2092 .expect("infallible");
2093 if let Some(h) = truncation_height {
2094 match tree.rollback_to_checkpoint(h)? {
2095 RollbackOutcome::RolledBack => (),
2096 RollbackOutcome::NoSuchCheckpoint => panic!("checkpoint must exist in this scope"),
2097 }
2098 }
2099
2100 Ok(())
2101}
2102
2103fn rollback_unscanned_tree_states<W>(wallet: &mut W) -> Result<(), SyncError<W::Error>>
2114where
2115 W: SyncWallet + SyncShardTrees,
2116{
2117 let Some(highest_scanned_height) = wallet
2118 .get_sync_state()
2119 .map_err(SyncError::WalletError)?
2120 .highest_scanned_height()
2121 else {
2122 return Ok(());
2123 };
2124
2125 for pool in [
2126 ShieldedPool::Sapling,
2127 ShieldedPool::Orchard,
2128 ShieldedPool::Ironwood,
2129 ] {
2130 let shard_trees = wallet
2131 .get_shard_trees_mut()
2132 .map_err(SyncError::WalletError)?;
2133 let rolled_back = match pool {
2134 ShieldedPool::Sapling => {
2135 rollback_unscanned_tree_state(highest_scanned_height, &mut shard_trees.sapling)
2136 }
2137 ShieldedPool::Orchard => {
2138 rollback_unscanned_tree_state(highest_scanned_height, &mut shard_trees.orchard)
2139 }
2140 ShieldedPool::Ironwood => {
2141 rollback_unscanned_tree_state(highest_scanned_height, &mut shard_trees.ironwood)
2142 }
2143 }?;
2144 if rolled_back {
2145 tracing::warn!(
2146 "{pool:?} shard tree held state above the highest scanned height \
2147 {highest_scanned_height} and was rolled back to its checkpoint at this height."
2148 );
2149 wallet.set_save_flag().map_err(SyncError::WalletError)?;
2150 }
2151 }
2152
2153 Ok(())
2154}
2155
2156fn rollback_unscanned_tree_state<H, const DEPTH: u8, const SHARD_HEIGHT: u8>(
2162 highest_scanned_height: BlockHeight,
2163 tree: &mut ShardTree<MemoryShardStore<H, BlockHeight>, DEPTH, SHARD_HEIGHT>,
2164) -> Result<bool, shardtree::error::ShardTreeError<Infallible>>
2165where
2166 H: incrementalmerkletree::Hashable + Clone + PartialEq,
2167{
2168 let Some(checkpoint) = truncate::plan_unscanned_state_rollback(
2169 truncate::tree_facts(tree, highest_scanned_height),
2170 highest_scanned_height,
2171 ) else {
2172 return Ok(false);
2173 };
2174
2175 match tree.rollback_to_checkpoint(checkpoint) {
2176 Ok(RollbackOutcome::RolledBack) => Ok(true),
2177 Ok(RollbackOutcome::NoSuchCheckpoint) => panic!("checkpoint must exist in this scope"),
2178 Err(shardtree::error::ShardTreeError::Query(
2179 shardtree::error::QueryError::CheckpointPruned,
2180 )) => {
2181 tracing::warn!(
2182 "shard tree holds state above the highest scanned height {highest_scanned_height} \
2183 and cannot roll back to its checkpoint at this height."
2184 );
2185 Ok(false)
2186 }
2187 Err(e) => Err(e),
2188 }
2189}
2190
2191async fn process_mempool_transaction<W>(
2195 consensus_parameters: &impl consensus::Parameters,
2196 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
2197 wallet: &mut W,
2198 raw_transaction: RawTransaction,
2199) -> Result<(), SyncError<W::Error>>
2200where
2201 W: SyncWallet + SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
2202{
2203 let mempool_height = wallet
2205 .get_sync_state()
2206 .map_err(SyncError::WalletError)?
2207 .last_known_chain_height()
2208 .expect("wallet height must exist after sync is initialised")
2209 + 1;
2210
2211 let transaction = zcash_primitives::transaction::Transaction::read(
2212 &raw_transaction.data[..],
2213 consensus::BranchId::for_height(consensus_parameters, mempool_height),
2214 )
2215 .map_err(ServerError::InvalidTransaction)?;
2216
2217 tracing::debug!(
2218 "mempool received txid {} at height {}",
2219 transaction.txid(),
2220 mempool_height
2221 );
2222
2223 if let Some(tx) = wallet
2224 .get_wallet_transactions_mut()
2225 .map_err(SyncError::WalletError)?
2226 .get_mut(&transaction.txid())
2227 {
2228 if !tx.status().is_failed() {
2232 tx.update_status(
2233 ConfirmationStatus::Mempool(mempool_height),
2234 SystemTime::now()
2235 .duration_since(SystemTime::UNIX_EPOCH)
2236 .expect("infalliable for such long time periods")
2237 .as_secs() as u32,
2238 false,
2239 );
2240
2241 return Ok(());
2242 }
2243 }
2244
2245 scan_pending_transaction(
2246 consensus_parameters,
2247 ufvks,
2248 wallet,
2249 transaction,
2250 ConfirmationStatus::Mempool(mempool_height),
2251 SystemTime::now()
2252 .duration_since(SystemTime::UNIX_EPOCH)
2253 .expect("infalliable for such long time periods")
2254 .as_secs() as u32,
2255 )?;
2256
2257 Ok(())
2258}
2259
2260fn truncate_wallet_data<W>(
2267 wallet: &mut W,
2268 truncate_height: BlockHeight,
2269) -> Result<(), SyncError<W::Error>>
2270where
2271 W: SyncWallet + SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
2272{
2273 let sync_state = wallet
2274 .get_sync_state_mut()
2275 .map_err(SyncError::WalletError)?;
2276 let wallet_state = truncate::WalletTruncationState {
2277 birthday: sync_state
2278 .wallet_birthday()
2279 .expect("should be non-empty in this scope"),
2280 highest_scanned_height: sync_state
2281 .highest_scanned_height()
2282 .expect("should be non-empty in this scope"),
2283 };
2284 match truncate::plan_truncation(wallet_state, truncate_height) {
2285 truncate::TruncationPlan::NoOp => Ok(()),
2286 truncate::TruncationPlan::ClearAll => {
2287 truncate_stores(wallet, consensus::H0, false)?;
2288 wallet.clear_shard_trees()
2289 }
2290 truncate::TruncationPlan::Truncate { height } => {
2291 truncate_stores(wallet, height, true)?;
2292 match wallet.truncate_shard_trees(height) {
2293 Ok(()) => Ok(()),
2294 Err(SyncError::TruncationError(height, pooltype)) => {
2295 clear_wallet_data(wallet)?;
2296
2297 Err(SyncError::TruncationError(height, pooltype))
2298 }
2299 Err(e) => Err(e),
2300 }
2301 }
2302 }
2303}
2304
2305fn truncate_stores<W>(
2311 wallet: &mut W,
2312 truncate_height: BlockHeight,
2313 set_truncated_transactions_failed: bool,
2314) -> Result<(), SyncError<W::Error>>
2315where
2316 W: SyncWallet + SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints,
2317{
2318 wallet
2319 .truncate_wallet_blocks(truncate_height)
2320 .map_err(SyncError::WalletError)?;
2321 wallet
2322 .truncate_wallet_transactions(truncate_height, set_truncated_transactions_failed)
2323 .map_err(SyncError::WalletError)?;
2324 wallet
2325 .truncate_nullifiers(truncate_height)
2326 .map_err(SyncError::WalletError)?;
2327 wallet
2328 .truncate_outpoints(truncate_height)
2329 .map_err(SyncError::WalletError)?;
2330 wallet
2333 .get_sync_state_mut()
2334 .map_err(SyncError::WalletError)?
2335 .scan_targets
2336 .retain(|scan_target| scan_target.block_height() <= truncate_height);
2337
2338 Ok(())
2339}
2340
2341fn clear_wallet_data<W>(wallet: &mut W) -> Result<(), SyncError<W::Error>>
2342where
2343 W: SyncWallet + SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
2344{
2345 let scan_targets = wallet
2346 .get_wallet_transactions()
2347 .map_err(SyncError::WalletError)?
2348 .values()
2349 .filter_map(|transaction| {
2350 transaction
2351 .status()
2352 .get_confirmed_height()
2353 .map(|height| ScanTarget::new(height, Some(transaction.txid())))
2354 })
2355 .collect::<Vec<_>>();
2356 truncate_wallet_data(wallet, consensus::H0)?;
2357 state::truncate_scan_ranges(
2358 consensus::H0,
2359 wallet
2360 .get_sync_state_mut()
2361 .map_err(SyncError::WalletError)?,
2362 );
2363 wallet
2364 .get_wallet_transactions_mut()
2365 .map_err(SyncError::WalletError)?
2366 .clear();
2367 let sync_state = wallet
2368 .get_sync_state_mut()
2369 .map_err(SyncError::WalletError)?;
2370 add_scan_targets(sync_state, &scan_targets);
2371 wallet.set_save_flag().map_err(SyncError::WalletError)?;
2372
2373 Ok(())
2374}
2375
2376#[allow(clippy::too_many_arguments)]
2378async fn update_wallet_data<W>(
2379 consensus_parameters: &(impl consensus::Parameters + Sync),
2380 wallet: &mut W,
2381 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
2382 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
2383 scan_range: &ScanRange,
2384 nullifiers: Option<&mut NullifierMap>,
2385 outpoints: &mut BTreeMap<OutputId, ScanTarget>,
2386 mut transactions: HashMap<TxId, WalletTransaction>,
2387 sapling_located_trees: Vec<LocatedTreeData<sapling_crypto::Node>>,
2388 orchard_located_trees: Vec<LocatedTreeData<MerkleHashOrchard>>,
2389 ironwood_located_trees: Vec<LocatedTreeData<MerkleHashOrchard>>,
2390 new_transparent_inuse_addresses: &HashMap<String, TransparentAddressId>,
2391) -> Result<(), SyncError<W::Error>>
2392where
2393 W: SyncWallet
2394 + SyncBlocks
2395 + SyncTransactions
2396 + SyncNullifiers
2397 + SyncOutPoints
2398 + SyncShardTrees
2399 + Send,
2400{
2401 let highest_scanned_height = wallet
2402 .get_sync_state()
2403 .map_err(SyncError::WalletError)?
2404 .highest_scanned_height()
2405 .expect("scan ranges should not be empty in this scope");
2406
2407 for transaction in transactions.values() {
2411 discover_unified_addresses(wallet, ufvks, transaction).map_err(SyncError::WalletError)?;
2412 }
2413 wallet
2414 .update_shard_trees(
2415 consensus_parameters,
2416 fetch_request_sender,
2417 scan_range,
2418 highest_scanned_height,
2419 witness::anchor_retention_policy(consensus_parameters),
2420 sapling_located_trees,
2421 orchard_located_trees,
2422 ironwood_located_trees,
2423 )
2424 .await?;
2425
2426 let sync_state = wallet
2427 .get_sync_state_mut()
2428 .map_err(SyncError::WalletError)?;
2429 for transaction in transactions.values() {
2430 state::update_found_note_shard_priority(
2431 consensus_parameters,
2432 sync_state,
2433 ShieldedPool::Sapling,
2434 transaction,
2435 );
2436 state::update_found_note_shard_priority(
2437 consensus_parameters,
2438 sync_state,
2439 ShieldedPool::Orchard,
2440 transaction,
2441 );
2442 state::update_found_note_shard_priority(
2443 consensus_parameters,
2444 sync_state,
2445 ShieldedPool::Ironwood,
2446 transaction,
2447 );
2448 }
2449 let refetch_nullifier_ranges = {
2457 let block_ranges: Vec<Range<BlockHeight>> = sync_state
2458 .scan_ranges()
2459 .iter()
2460 .filter(|&scan_range| {
2461 scan_range.priority() == ScanPriority::ScannedWithoutMapping
2462 || scan_range.priority() == ScanPriority::RefetchingNullifiers
2463 })
2464 .map(|scan_range| scan_range.block_range().clone())
2465 .collect();
2466
2467 block_ranges
2468 [block_ranges.partition_point(|range| range.start < scan_range.block_range().end)..]
2469 .to_vec()
2470 };
2471 for transaction in transactions.values_mut() {
2472 for note in transaction.sapling_notes.as_mut_slice() {
2473 note.refetch_nullifier_ranges = refetch_nullifier_ranges.clone();
2474 }
2475 for note in transaction.orchard_notes.as_mut_slice() {
2476 note.refetch_nullifier_ranges = refetch_nullifier_ranges.clone();
2477 }
2478 for note in transaction.ironwood_notes.as_mut_slice() {
2479 note.refetch_nullifier_ranges = refetch_nullifier_ranges.clone();
2480 }
2481 }
2482
2483 wallet
2484 .extend_wallet_transactions(transactions)
2485 .map_err(SyncError::WalletError)?;
2486 if let Some(nullifiers) = nullifiers {
2487 wallet
2488 .append_nullifiers(nullifiers)
2489 .map_err(SyncError::WalletError)?;
2490 }
2491 wallet
2492 .append_outpoints(outpoints)
2493 .map_err(SyncError::WalletError)?;
2494 let wallet_transparent_addresses = wallet
2495 .get_transparent_addresses_mut()
2496 .map_err(SyncError::WalletError)?;
2497 for (address, id) in new_transparent_inuse_addresses {
2498 wallet_transparent_addresses.insert(*id, address.clone());
2499 }
2500
2501 Ok(())
2502}
2503
2504fn discover_unified_addresses<W>(
2505 wallet: &mut W,
2506 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
2507 transaction: &WalletTransaction,
2508) -> Result<(), W::Error>
2509where
2510 W: SyncWallet,
2511{
2512 for note in transaction
2513 .orchard_notes()
2514 .iter()
2515 .filter(|¬e| note.key_id().scope == zip32::Scope::External)
2516 {
2517 let ivk = ufvks
2518 .get(¬e.key_id().account_id())
2519 .expect("ufvk must exist to decrypt this note")
2520 .orchard()
2521 .expect("fvk must exist to decrypt this note")
2522 .to_ivk(zip32::Scope::External);
2523
2524 wallet.add_orchard_address(
2525 note.key_id().account_id(),
2526 note.note().recipient(),
2527 ivk.diversifier_index(¬e.note().recipient())
2528 .expect("must be key used to create this address"),
2529 )?;
2530 }
2531 for note in transaction
2533 .ironwood_notes()
2534 .iter()
2535 .filter(|¬e| note.key_id().scope == zip32::Scope::External)
2536 {
2537 let ivk = ufvks
2538 .get(¬e.key_id().account_id())
2539 .expect("ufvk must exist to decrypt this note")
2540 .orchard()
2541 .expect("fvk must exist to decrypt this note")
2542 .to_ivk(zip32::Scope::External);
2543
2544 wallet.add_orchard_address(
2545 note.key_id().account_id(),
2546 note.note().recipient(),
2547 ivk.diversifier_index(¬e.note().recipient())
2548 .expect("must be key used to create this address"),
2549 )?;
2550 }
2551 for note in transaction
2552 .sapling_notes()
2553 .iter()
2554 .filter(|¬e| note.key_id().scope == zip32::Scope::External)
2555 {
2556 let ivk = ufvks
2557 .get(¬e.key_id().account_id())
2558 .expect("ufvk must exist to decrypt this note")
2559 .sapling()
2560 .expect("fvk must exist to decrypt this note")
2561 .to_external_ivk();
2562
2563 wallet.add_sapling_address(
2564 note.key_id().account_id(),
2565 note.note().recipient(),
2566 ivk.decrypt_diversifier(¬e.note().recipient())
2567 .expect("must be key used to create this address"),
2568 )?;
2569 }
2570
2571 Ok(())
2572}
2573
2574fn remove_irrelevant_data<W>(wallet: &mut W) -> Result<(), W::Error>
2575where
2576 W: SyncWallet + SyncBlocks + SyncOutPoints + SyncNullifiers + SyncTransactions,
2577{
2578 let fully_scanned_height = wallet
2579 .get_sync_state()?
2580 .fully_scanned_height()
2581 .expect("scan ranges must be non-empty");
2582
2583 wallet
2584 .get_outpoints_mut()?
2585 .retain(|_, scan_target| scan_target.block_height() > fully_scanned_height);
2586 wallet
2587 .get_nullifiers_mut()?
2588 .sapling
2589 .retain(|_, scan_target| scan_target.block_height() > fully_scanned_height);
2590 wallet
2591 .get_nullifiers_mut()?
2592 .orchard
2593 .retain(|_, scan_target| scan_target.block_height() > fully_scanned_height);
2594 wallet
2595 .get_nullifiers_mut()?
2596 .ironwood
2597 .retain(|_, scan_target| scan_target.block_height() > fully_scanned_height);
2598 state::drop_settled_scan_targets(wallet.get_sync_state_mut()?, fully_scanned_height);
2599 remove_irrelevant_blocks(wallet)?;
2600
2601 Ok(())
2602}
2603
2604fn remove_irrelevant_blocks<W>(wallet: &mut W) -> Result<(), W::Error>
2605where
2606 W: SyncWallet + SyncBlocks + SyncTransactions,
2607{
2608 let sync_state = wallet.get_sync_state()?;
2609 let highest_scanned_height = sync_state
2610 .highest_scanned_height()
2611 .expect("should be non-empty");
2612 let scanned_range_bounds = sync_state
2613 .scan_ranges()
2614 .iter()
2615 .filter(|scan_range| scan_range.priority().is_scanned())
2616 .flat_map(|scanned_range| {
2617 vec![
2618 scanned_range.block_range().start,
2619 scanned_range.block_range().end - 1,
2620 ]
2621 })
2622 .collect::<Vec<_>>();
2623 let wallet_transaction_heights = wallet
2624 .get_wallet_transactions()?
2625 .values()
2626 .filter_map(|tx| tx.status().get_confirmed_height())
2627 .collect::<Vec<_>>();
2628
2629 wallet.get_wallet_blocks_mut()?.retain(|height, _| {
2630 *height >= highest_scanned_height.saturating_sub(MAX_REORG_ALLOWANCE)
2631 || scanned_range_bounds.contains(height)
2632 || wallet_transaction_heights.contains(height)
2633 });
2634
2635 Ok(())
2636}
2637
2638fn add_scanned_blocks<W>(
2639 wallet: &mut W,
2640 mut scanned_blocks: BTreeMap<BlockHeight, WalletBlock>,
2641 scan_range: &ScanRange,
2642) -> Result<(), W::Error>
2643where
2644 W: SyncWallet + SyncBlocks + SyncTransactions,
2645{
2646 let sync_state = wallet.get_sync_state()?;
2647 let highest_scanned_height = sync_state
2648 .highest_scanned_height()
2649 .expect("scan ranges must be non-empty");
2650
2651 let wallet_transaction_heights = wallet
2652 .get_wallet_transactions()?
2653 .values()
2654 .filter_map(|tx| tx.status().get_confirmed_height())
2655 .collect::<Vec<_>>();
2656
2657 scanned_blocks.retain(|height, _| {
2658 *height >= highest_scanned_height.saturating_sub(MAX_REORG_ALLOWANCE)
2659 || *height == scan_range.block_range().start
2660 || *height == scan_range.block_range().end - 1
2661 || wallet_transaction_heights.contains(height)
2662 });
2663
2664 wallet.append_wallet_blocks(scanned_blocks)?;
2665
2666 Ok(())
2667}
2668
2669#[derive(Debug, Clone, Copy)]
2671struct SubtreeRootFetchStart {
2672 sapling_start_index: u32,
2673 orchard_start_index: u32,
2674 ironwood_start_index: u32,
2675}
2676
2677#[derive(Debug)]
2679struct FetchedSubtreeRoots {
2680 fetch_start: SubtreeRootFetchStart,
2681 sapling: Vec<SubtreeRoot>,
2682 orchard: Vec<SubtreeRoot>,
2683 ironwood: Vec<SubtreeRoot>,
2684}
2685
2686fn subtree_root_fetch_start<W>(wallet: &W) -> Result<SubtreeRootFetchStart, SyncError<W::Error>>
2687where
2688 W: SyncWallet + SyncShardTrees,
2689{
2690 let shard_trees = wallet.get_shard_trees().map_err(SyncError::WalletError)?;
2691 let sync_state = wallet.get_sync_state().map_err(SyncError::WalletError)?;
2692 Ok(SubtreeRootFetchStart {
2693 sapling_start_index: subtree_root_fetch_index(
2694 &shard_trees.sapling,
2695 sync_state.sapling_shard_ranges(),
2696 ),
2697 orchard_start_index: subtree_root_fetch_index(
2698 &shard_trees.orchard,
2699 sync_state.orchard_shard_ranges(),
2700 ),
2701 ironwood_start_index: subtree_root_fetch_index(
2702 &shard_trees.ironwood,
2703 sync_state.ironwood_shard_ranges(),
2704 ),
2705 })
2706}
2707
2708async fn fetch_subtree_roots(
2709 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
2710 fetch_start: SubtreeRootFetchStart,
2711) -> Result<FetchedSubtreeRoots, ServerError> {
2712 let (sapling_subtree_roots, orchard_subtree_roots, ironwood_subtree_roots) = futures::join!(
2713 client::get_subtree_roots(
2714 fetch_request_sender.clone(),
2715 fetch_start.sapling_start_index,
2716 0,
2717 0
2718 ),
2719 client::get_subtree_roots(
2720 fetch_request_sender.clone(),
2721 fetch_start.orchard_start_index,
2722 1,
2723 0
2724 ),
2725 client::get_subtree_roots(fetch_request_sender, fetch_start.ironwood_start_index, 2, 0)
2726 );
2727
2728 Ok(FetchedSubtreeRoots {
2729 fetch_start,
2730 sapling: sapling_subtree_roots?,
2731 orchard: orchard_subtree_roots?,
2732 ironwood: ironwood_subtree_roots?,
2733 })
2734}
2735
2736fn add_fetched_subtree_roots<W>(
2737 consensus_parameters: &impl consensus::Parameters,
2738 wallet: &mut W,
2739 subtree_roots: FetchedSubtreeRoots,
2740) -> Result<(), SyncError<W::Error>>
2741where
2742 W: SyncWallet + SyncShardTrees,
2743{
2744 let FetchedSubtreeRoots {
2745 fetch_start:
2746 SubtreeRootFetchStart {
2747 sapling_start_index,
2748 orchard_start_index,
2749 ironwood_start_index,
2750 },
2751 sapling: sapling_subtree_roots,
2752 orchard: orchard_subtree_roots,
2753 ironwood: ironwood_subtree_roots,
2754 } = subtree_roots;
2755
2756 let sync_state = wallet
2757 .get_sync_state_mut()
2758 .map_err(SyncError::WalletError)?;
2759 for (pool, start_index, subtree_roots) in [
2760 (
2761 ShieldedPool::Sapling,
2762 sapling_start_index,
2763 &sapling_subtree_roots,
2764 ),
2765 (
2766 ShieldedPool::Orchard,
2767 orchard_start_index,
2768 &orchard_subtree_roots,
2769 ),
2770 (
2771 ShieldedPool::Ironwood,
2772 ironwood_start_index,
2773 &ironwood_subtree_roots,
2774 ),
2775 ] {
2776 state::add_shard_ranges(
2777 consensus_parameters,
2778 pool,
2779 sync_state,
2780 start_index,
2781 subtree_roots,
2782 );
2783 }
2784
2785 let shard_trees = wallet
2786 .get_shard_trees_mut()
2787 .map_err(SyncError::WalletError)?;
2788 witness::add_subtree_roots(
2789 sapling_start_index as usize,
2790 sapling_subtree_roots,
2791 &mut shard_trees.sapling,
2792 )?;
2793 witness::add_subtree_roots(
2794 orchard_start_index as usize,
2795 orchard_subtree_roots,
2796 &mut shard_trees.orchard,
2797 )?;
2798 witness::add_subtree_roots(
2799 ironwood_start_index as usize,
2800 ironwood_subtree_roots,
2801 &mut shard_trees.ironwood,
2802 )?;
2803 wallet.set_save_flag().map_err(SyncError::WalletError)?;
2804
2805 Ok(())
2806}
2807
2808fn subtree_root_fetch_index<H, const DEPTH: u8, const SHARD_HEIGHT: u8>(
2809 shard_tree: &ShardTree<MemoryShardStore<H, BlockHeight>, DEPTH, SHARD_HEIGHT>,
2810 shard_ranges: &[Range<BlockHeight>],
2811) -> u32
2812where
2813 H: incrementalmerkletree::Hashable + Clone + PartialEq,
2814{
2815 let newest_recorded_root = shard_ranges.len().saturating_sub(1) as u32;
2821 witness::subtree_fetch_start_index(shard_tree).min(newest_recorded_root)
2822}
2823
2824fn repin_anchor_checkpoints<W>(
2827 consensus_parameters: &impl consensus::Parameters,
2828 wallet: &mut W,
2829) -> Result<(), SyncError<W::Error>>
2830where
2831 W: SyncWallet + SyncShardTrees,
2832{
2833 let Some(policy) = witness::anchor_retention_policy(consensus_parameters) else {
2834 return Ok(());
2835 };
2836 let Some(highest_scanned_height) = wallet
2837 .get_sync_state()
2838 .map_err(SyncError::WalletError)?
2839 .highest_scanned_height()
2840 else {
2841 return Ok(());
2842 };
2843 let window = witness::anchor_retention_window(&policy, highest_scanned_height);
2844 let shard_trees = wallet
2845 .get_shard_trees_mut()
2846 .map_err(SyncError::WalletError)?;
2847 witness::repin_anchor_checkpoints(&policy, &window, shard_trees.sapling.store_mut());
2848 witness::repin_anchor_checkpoints(&policy, &window, shard_trees.orchard.store_mut());
2849 witness::repin_anchor_checkpoints(&policy, &window, shard_trees.ironwood.store_mut());
2850
2851 Ok(())
2852}
2853
2854fn initial_frontier_height<W>(
2857 consensus_parameters: &impl consensus::Parameters,
2858 wallet: &W,
2859) -> Result<Option<BlockHeight>, SyncError<W::Error>>
2860where
2861 W: SyncWallet + SyncShardTrees,
2862{
2863 let birthday = wallet.get_birthday().map_err(SyncError::WalletError)?;
2864 if birthday
2865 == consensus_parameters
2866 .activation_height(consensus::NetworkUpgrade::Sapling)
2867 .expect("sapling activation height should always return Some")
2868 {
2869 return Ok(None);
2870 }
2871
2872 let shard_trees = wallet.get_shard_trees().map_err(SyncError::WalletError)?;
2873 Ok((shard_trees
2874 .sapling
2875 .store()
2876 .checkpoint_count()
2877 .expect("infallible")
2878 == 1)
2879 .then_some(birthday))
2880}
2881
2882fn insert_initial_frontier<W>(
2883 wallet: &mut W,
2884 birthday: BlockHeight,
2885 frontiers: &witness::Frontiers,
2886) -> Result<(), SyncError<W::Error>>
2887where
2888 W: SyncWallet + SyncShardTrees,
2889{
2890 let shard_trees = wallet
2891 .get_shard_trees_mut()
2892 .map_err(SyncError::WalletError)?;
2893 shard_trees
2894 .sapling
2895 .insert_frontier(
2896 frontiers.final_sapling_tree().clone(),
2897 Retention::Checkpoint {
2898 id: birthday,
2899 marking: Marking::None,
2900 },
2901 )
2902 .expect("infallible");
2903 shard_trees
2904 .orchard
2905 .insert_frontier(
2906 frontiers.final_orchard_tree().clone(),
2907 Retention::Checkpoint {
2908 id: birthday,
2909 marking: Marking::None,
2910 },
2911 )
2912 .expect("infallible");
2913 shard_trees
2914 .ironwood
2915 .insert_frontier(
2916 frontiers.final_ironwood_tree().clone(),
2917 Retention::Checkpoint {
2918 id: birthday,
2919 marking: Marking::None,
2920 },
2921 )
2922 .expect("infallible");
2923 wallet.set_save_flag().map_err(SyncError::WalletError)?;
2924
2925 Ok(())
2926}
2927
2928async fn mempool_monitor<C>(
2934 mut client: C,
2935 mempool_transaction_sender: mpsc::Sender<MempoolMessage>,
2936 unprocessed_transactions_count: Arc<AtomicU32>,
2937 stream_connected_at: Arc<std::sync::OnceLock<std::time::Instant>>,
2938 shutdown_mempool: Arc<AtomicBool>,
2939) -> Result<(), MempoolError>
2940where
2941 C: Clone + Indexer + TransparentIndexer + Sync + Send + 'static,
2942{
2943 let mut interval = tokio::time::interval(Duration::from_millis(50));
2947 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
2948 'main: loop {
2949 if shutdown_mempool.load(atomic::Ordering::Acquire) {
2951 break 'main;
2952 }
2953
2954 let response =
2955 client::get_mempool_transaction_stream(&mut client, shutdown_mempool.clone()).await;
2956
2957 match response {
2958 Ok(mut mempool_stream) => {
2959 let _already_set = stream_connected_at.set(std::time::Instant::now());
2964 interval.reset();
2965 loop {
2966 tokio::select! {
2967 mempool_stream_message = mempool_stream.message() => {
2968 match mempool_stream_message {
2969 Ok(Some(raw_transaction)) => {
2970 unprocessed_transactions_count.fetch_add(1, atomic::Ordering::Release);
2973 match mempool_transaction_sender
2974 .send(MempoolMessage::Transaction(raw_transaction))
2975 .await {
2976 Ok(_) => (),
2977 Err(_) => {
2978 unprocessed_transactions_count.store(0, atomic::Ordering::Release);
2979 shutdown_mempool.store(true, atomic::Ordering::Release);
2980 break 'main;
2981 }
2982 }
2983 }
2984 Ok(None) => {
2985 match mempool_transaction_sender
2986 .send(MempoolMessage::NewBlockMined)
2987 .await {
2988 Ok(_) => {
2989 continue 'main;
2990 }
2991 Err(_) => {
2992 unprocessed_transactions_count.store(0, atomic::Ordering::Release);
2993 shutdown_mempool.store(true, atomic::Ordering::Release);
2994 break 'main;
2995 }
2996 }
2997 }
2998 Err(e) => {
2999 tracing::warn!("Mempool error: {e}");
3000 continue 'main;
3001 }
3002 }
3003
3004 }
3005
3006 _ = interval.tick() => {
3007 if shutdown_mempool.load(atomic::Ordering::Acquire) {
3008 break 'main;
3009 }
3010 }
3011 }
3012 }
3013 }
3014 Err(e @ MempoolError::ShutdownWithoutStream) => return Err(e),
3015 Err(MempoolError::ServerError(e)) => {
3016 tracing::warn!(
3017 "Mempool stream request failed! Status: {}.\nRetrying...",
3018 crate::error::cause_chain_text(&e)
3019 );
3020 tokio::time::sleep(Duration::from_secs(3)).await;
3021 }
3022 }
3023 }
3024
3025 Ok(())
3026}
3027
3028fn expire_transactions<W>(wallet: &mut W) -> Result<(), SyncError<W::Error>>
3036where
3037 W: SyncWallet + SyncTransactions,
3038{
3039 let Some(fully_scanned_height) = wallet
3040 .get_sync_state()
3041 .map_err(SyncError::WalletError)?
3042 .fully_scanned_height()
3043 else {
3044 return Ok(());
3045 };
3046 let wallet_transactions = wallet
3047 .get_wallet_transactions_mut()
3048 .map_err(SyncError::WalletError)?;
3049
3050 let expired_txids = wallet_transactions
3051 .values()
3052 .filter(|transaction| {
3053 let expiry_height = transaction.transaction().expiry_height();
3054 transaction.status().is_pending()
3055 && expiry_height > BlockHeight::from_u32(0)
3056 && fully_scanned_height >= expiry_height
3057 })
3058 .map(super::wallet::WalletTransaction::txid)
3059 .collect::<Vec<_>>();
3060 set_transactions_failed(wallet_transactions, expired_txids);
3061 wallet.set_save_flag().map_err(SyncError::WalletError)?;
3062
3063 Ok(())
3064}
3065
3066fn max_nullifier_map_size(performance_level: PerformanceLevel) -> Option<usize> {
3067 match performance_level {
3068 PerformanceLevel::Low => Some(0),
3069 PerformanceLevel::Medium => Some(125_000),
3070 PerformanceLevel::High => Some(2_000_000),
3071 PerformanceLevel::Maximum => None,
3072 }
3073}
3074
3075#[cfg(test)]
3076mod test {
3077 use zcash_protocol::consensus::BlockHeight;
3078 use zcash_protocol::local_consensus::LocalNetwork;
3079
3080 const fn activated_at(height: u32) -> LocalNetwork {
3082 let activation = Some(BlockHeight::from_u32(height));
3083 LocalNetwork {
3084 overwinter: activation,
3085 sapling: activation,
3086 blossom: activation,
3087 heartwood: activation,
3088 canopy: activation,
3089 nu5: activation,
3090 nu6: activation,
3091 nu6_1: activation,
3092 nu6_2: activation,
3093 nu6_3: activation,
3094 nu7: activation,
3095 }
3096 }
3097
3098 const NETWORK: LocalNetwork = activated_at(1);
3100
3101 mod sync_status_completion {
3107 use zcash_protocol::consensus::BlockHeight;
3108
3109 use crate::sync::{ScanPriority, ScanRange, SyncStatus};
3110
3111 fn status(sync_start_height: u32, priorities: &[ScanPriority]) -> SyncStatus {
3115 let scan_ranges = priorities
3116 .iter()
3117 .enumerate()
3118 .map(|(index, priority)| {
3119 let start = 1_000 + 10 * index as u32;
3120 ScanRange::from_parts(
3121 BlockHeight::from(start)..BlockHeight::from(start + 10),
3122 *priority,
3123 )
3124 })
3125 .collect();
3126 SyncStatus {
3127 scan_ranges,
3128 sync_start_height: sync_start_height.into(),
3129 session_blocks_scanned: 0,
3130 total_blocks_scanned: 0,
3131 total_blocks: 0,
3132 session_sapling_outputs_scanned: 0,
3133 total_sapling_outputs_scanned: 0,
3134 session_orchard_outputs_scanned: 0,
3135 total_orchard_outputs_scanned: 0,
3136 session_ironwood_outputs_scanned: 0,
3137 total_ironwood_outputs_scanned: 0,
3138 total_outputs_scanned: 0,
3139 total_outputs: 0,
3140 }
3141 }
3142
3143 #[test]
3144 fn never_started_is_not_complete() {
3145 assert!(!status(0, &[]).is_complete());
3146 }
3147
3148 #[test]
3152 fn started_with_no_scan_ranges_is_complete() {
3153 assert!(status(1_000, &[]).is_complete());
3154 }
3155
3156 #[test]
3157 fn all_ranges_scanned_is_complete() {
3158 assert!(status(1_000, &[ScanPriority::Scanned, ScanPriority::Scanned]).is_complete());
3159 }
3160
3161 #[test]
3165 fn output_free_range_is_complete() {
3166 let status = status(1_000, &[ScanPriority::Scanned]);
3167 assert_eq!(status.total_outputs, 0);
3168 assert!(status.is_complete());
3169 }
3170
3171 #[test]
3172 fn nullifier_retrieval_pending_is_not_complete() {
3173 for pending in [
3174 ScanPriority::ScannedWithoutMapping,
3175 ScanPriority::RefetchingNullifiers,
3176 ] {
3177 assert!(!status(1_000, &[ScanPriority::Scanned, pending]).is_complete());
3178 }
3179 }
3180
3181 #[test]
3185 fn unscanned_range_is_not_complete() {
3186 let mut status = status(1_000, &[ScanPriority::Scanned, ScanPriority::Historic]);
3187 status.total_outputs_scanned = 10_000;
3188 status.total_outputs = 10_000;
3189 assert!(!status.is_complete());
3190 }
3191 }
3192
3193 mod truncation {
3201 use std::collections::BTreeMap;
3202
3203 use zcash_primitives::{block::BlockHash, transaction::TxId};
3204 use zcash_protocol::consensus::BlockHeight;
3205
3206 use crate::mocks::MockWalletBuilder;
3207 use crate::shardtree_ext::{CheckpointAppendOutcome, ShardTreeExt};
3208 use crate::sync::{ScanPriority, ScanRange, truncate_wallet_data};
3209 use crate::wallet::{
3210 ScanTarget, ShardTrees, SyncState, TreeBounds, WalletBlock,
3211 traits::{SyncBlocks, SyncWallet},
3212 };
3213
3214 fn block(height: u32) -> WalletBlock {
3216 WalletBlock {
3217 block_height: BlockHeight::from_u32(height),
3218 block_hash: BlockHash([0; 32]),
3219 prev_hash: BlockHash([0; 32]),
3220 time: 0,
3221 txids: Vec::new(),
3222 tree_bounds: TreeBounds {
3223 sapling_initial_tree_size: 0,
3224 sapling_final_tree_size: 0,
3225 orchard_initial_tree_size: 0,
3226 orchard_final_tree_size: 0,
3227 ironwood_initial_tree_size: 0,
3228 ironwood_final_tree_size: 0,
3229 },
3230 }
3231 }
3232
3233 fn synced_wallet(shard_trees: ShardTrees) -> crate::mocks::MockWallet {
3237 let wallet_blocks: BTreeMap<_, _> = (6..=10u32)
3238 .map(|height| (BlockHeight::from_u32(height), block(height)))
3239 .collect();
3240 let sync_state = SyncState::new_for_test(vec![ScanRange::from_parts(
3241 BlockHeight::from_u32(6)..BlockHeight::from_u32(11),
3242 ScanPriority::Scanned,
3243 )]);
3244 MockWalletBuilder::new()
3245 .birthday(BlockHeight::from_u32(6))
3246 .sync_state(sync_state)
3247 .wallet_blocks(wallet_blocks)
3248 .shard_trees(shard_trees)
3249 .create_mock_wallet()
3250 }
3251
3252 #[test]
3260 fn migrated_wallet_survives_reorg_truncation() {
3261 let mut shard_trees = ShardTrees::new();
3267 for height in 6..=10u32 {
3268 assert_eq!(
3269 shard_trees
3270 .sapling
3271 .append_checkpoint(BlockHeight::from_u32(height))
3272 .unwrap(),
3273 CheckpointAppendOutcome::Appended
3274 );
3275 assert_eq!(
3276 shard_trees
3277 .orchard
3278 .append_checkpoint(BlockHeight::from_u32(height))
3279 .unwrap(),
3280 CheckpointAppendOutcome::Appended
3281 );
3282 }
3283 let mut wallet = synced_wallet(shard_trees);
3284 let kept_target =
3285 ScanTarget::internal(BlockHeight::from_u32(7), TxId::from_bytes([7; 32]));
3286 let evicted_target =
3287 ScanTarget::internal(BlockHeight::from_u32(9), TxId::from_bytes([9; 32]));
3288 for scan_target in [kept_target, evicted_target] {
3289 wallet
3290 .get_sync_state_mut()
3291 .unwrap()
3292 .insert_scan_target(scan_target);
3293 }
3294
3295 let result = truncate_wallet_data(&mut wallet, BlockHeight::from_u32(8));
3297
3298 assert!(
3299 result.is_ok(),
3300 "reorg truncation wiped a healthy migrated wallet: {result:?}"
3301 );
3302 assert!(wallet.get_wallet_block(BlockHeight::from_u32(6)).is_ok());
3305 assert_eq!(
3307 wallet
3308 .get_sync_state()
3309 .unwrap()
3310 .scan_targets
3311 .iter()
3312 .copied()
3313 .collect::<Vec<_>>(),
3314 [kept_target]
3315 );
3316 assert!(wallet.get_wallet_block(BlockHeight::from_u32(8)).is_ok());
3317 assert!(wallet.get_wallet_block(BlockHeight::from_u32(9)).is_err());
3318 }
3319
3320 #[test]
3324 fn unrecoverable_tree_still_clears_wallet_data() {
3325 let mut shard_trees = ShardTrees::new();
3329 for height in 6..=10u32 {
3330 assert_eq!(
3331 shard_trees
3332 .sapling
3333 .append_checkpoint(BlockHeight::from_u32(height))
3334 .unwrap(),
3335 CheckpointAppendOutcome::Appended
3336 );
3337 }
3338 for height in 9..=10u32 {
3339 assert_eq!(
3340 shard_trees
3341 .orchard
3342 .append_checkpoint(BlockHeight::from_u32(height))
3343 .unwrap(),
3344 CheckpointAppendOutcome::Appended
3345 );
3346 }
3347 let mut wallet = synced_wallet(shard_trees);
3348
3349 let result = truncate_wallet_data(&mut wallet, BlockHeight::from_u32(8));
3350
3351 assert!(matches!(
3352 result,
3353 Err(crate::error::SyncError::TruncationError(_, _))
3354 ));
3355 assert!(wallet.get_wallet_block(BlockHeight::from_u32(6)).is_err());
3357 }
3358
3359 fn assert_every_tree_holds_a_checkpoint(wallet: &mut crate::mocks::MockWallet) {
3361 use crate::wallet::traits::SyncShardTrees;
3362 use shardtree::store::ShardStore as _;
3363
3364 let shard_trees = wallet.get_shard_trees_mut().unwrap();
3365 let sapling = shard_trees.sapling.store().max_checkpoint_id().unwrap();
3366 let orchard = shard_trees.orchard.store().max_checkpoint_id().unwrap();
3367 let ironwood = shard_trees.ironwood.store().max_checkpoint_id().unwrap();
3368 assert!(
3369 sapling.is_some() && orchard.is_some() && ironwood.is_some(),
3370 "a shard tree lost its last checkpoint: sapling {sapling:?}, orchard {orchard:?}, ironwood {ironwood:?}"
3371 );
3372 }
3373
3374 #[test]
3376 fn clear_all_truncation_leaves_a_checkpoint_in_every_tree() {
3377 let mut shard_trees = ShardTrees::new();
3378 for height in 6..=10u32 {
3379 shard_trees
3380 .sapling
3381 .append_checkpoint(BlockHeight::from_u32(height))
3382 .unwrap();
3383 }
3384 let mut wallet = synced_wallet(shard_trees);
3385
3386 truncate_wallet_data(&mut wallet, BlockHeight::from_u32(3)).unwrap();
3387
3388 assert!(wallet.get_wallet_block(BlockHeight::from_u32(6)).is_err());
3389 assert_every_tree_holds_a_checkpoint(&mut wallet);
3390 }
3391
3392 #[test]
3394 fn rescan_recovery_leaves_a_checkpoint_in_every_tree() {
3395 let mut shard_trees = ShardTrees::new();
3396 for height in 9..=10u32 {
3397 shard_trees
3398 .orchard
3399 .append_checkpoint(BlockHeight::from_u32(height))
3400 .unwrap();
3401 }
3402 let mut wallet = synced_wallet(shard_trees);
3403
3404 let result = truncate_wallet_data(&mut wallet, BlockHeight::from_u32(8));
3405
3406 assert!(matches!(
3407 result,
3408 Err(crate::error::SyncError::TruncationError(_, _))
3409 ));
3410 assert_every_tree_holds_a_checkpoint(&mut wallet);
3411 }
3412 }
3413
3414 mod unscanned_shard_tree_state {
3418 use std::convert::Infallible;
3419
3420 use incrementalmerkletree::{Marking, Position, Retention};
3421 use orchard::tree::MerkleHashOrchard;
3422 use shardtree::error::ShardTreeError;
3423 use shardtree::store::{Checkpoint, ShardStore as _};
3424 use zcash_protocol::consensus::BlockHeight;
3425 use zingo_netutils::lightwallet_protocol::SubtreeRoot;
3426
3427 use crate::mocks::{MockWallet, MockWalletBuilder};
3428 use crate::sync::{ScanPriority, ScanRange, rollback_unscanned_tree_states};
3429 use crate::wallet::{ShardTrees, SyncState, traits::SyncShardTrees};
3430 use crate::witness;
3431
3432 const BIRTHDAY: u32 = 6;
3433 const HIGHEST_SCANNED: u32 = 10;
3434 const NEXT_BLOCK: u32 = HIGHEST_SCANNED + 1;
3435 const NEXT_BLOCK_POSITION: u64 = (NEXT_BLOCK - BIRTHDAY) as u64;
3437
3438 const SCANNED_CHAIN: u8 = 1;
3439 const STALE_BLOCK: u8 = 2;
3440 const REPLACING_BLOCK: u8 = 3;
3441
3442 fn note_commitment(chain_tag: u8) -> MerkleHashOrchard {
3444 let mut bytes = [0; 32];
3445 bytes[0] = chain_tag;
3446 MerkleHashOrchard::from_bytes(&bytes).expect("a small value is a valid note commitment")
3447 }
3448
3449 fn block_leaf(chain_tag: u8, height: u32) -> (MerkleHashOrchard, Retention<BlockHeight>) {
3452 (
3453 note_commitment(chain_tag),
3454 Retention::Checkpoint {
3455 id: BlockHeight::from_u32(height),
3456 marking: Marking::None,
3457 },
3458 )
3459 }
3460
3461 fn scanned_shard_trees() -> ShardTrees {
3464 let mut shard_trees = ShardTrees::new();
3465 for height in BIRTHDAY..=HIGHEST_SCANNED {
3466 let (leaf, retention) = block_leaf(SCANNED_CHAIN, height);
3467 shard_trees.orchard.append(leaf, retention).unwrap();
3468 }
3469
3470 shard_trees
3471 }
3472
3473 fn insert_next_block(
3475 shard_trees: &mut ShardTrees,
3476 chain_tag: u8,
3477 ) -> Result<(), ShardTreeError<Infallible>> {
3478 const LOCATED_TREE_SIZE: usize = 1;
3479 for located_tree in witness::build_located_trees(
3480 Position::from(NEXT_BLOCK_POSITION),
3481 vec![block_leaf(chain_tag, NEXT_BLOCK)],
3482 LOCATED_TREE_SIZE,
3483 ) {
3484 shard_trees
3485 .orchard
3486 .insert_tree(located_tree.subtree, located_tree.checkpoints)?;
3487 }
3488
3489 Ok(())
3490 }
3491
3492 fn wallet(shard_trees: ShardTrees) -> MockWallet {
3495 let sync_state = SyncState::new_for_test(vec![
3496 ScanRange::from_parts(
3497 BlockHeight::from_u32(BIRTHDAY)..BlockHeight::from_u32(NEXT_BLOCK),
3498 ScanPriority::Scanned,
3499 ),
3500 ScanRange::from_parts(
3501 BlockHeight::from_u32(NEXT_BLOCK)..BlockHeight::from_u32(NEXT_BLOCK + 1),
3502 ScanPriority::Verify,
3503 ),
3504 ]);
3505 MockWalletBuilder::new()
3506 .birthday(BlockHeight::from_u32(BIRTHDAY))
3507 .sync_state(sync_state)
3508 .shard_trees(shard_trees)
3509 .create_mock_wallet()
3510 }
3511
3512 #[test]
3517 fn stale_block_is_rolled_back_and_its_replacement_inserts() {
3518 let mut shard_trees = scanned_shard_trees();
3519 insert_next_block(&mut shard_trees, STALE_BLOCK).unwrap();
3520 assert!(
3521 insert_next_block(&mut shard_trees, REPLACING_BLOCK).is_err(),
3522 "the replacing block must conflict with the stale block for this test to hold"
3523 );
3524 let mut wallet = wallet(shard_trees);
3525
3526 rollback_unscanned_tree_states(&mut wallet).unwrap();
3527
3528 let shard_trees = wallet.get_shard_trees_mut().unwrap();
3529 assert_eq!(
3530 shard_trees.orchard.max_leaf_position(None).unwrap(),
3531 Some(Position::from(NEXT_BLOCK_POSITION - 1)),
3532 "only the note commitments of scanned blocks are kept"
3533 );
3534 assert_eq!(
3535 shard_trees.orchard.store().max_checkpoint_id().unwrap(),
3536 Some(BlockHeight::from_u32(HIGHEST_SCANNED))
3537 );
3538 insert_next_block(shard_trees, REPLACING_BLOCK)
3539 .expect("the replacing block inserts into the rolled back tree");
3540 }
3541
3542 #[test]
3545 fn scanned_shard_trees_are_left_as_they_are() {
3546 const SUBTREE_ABOVE_HIGHEST_SCANNED: usize = 1;
3547 let mut shard_trees = scanned_shard_trees();
3548 witness::add_subtree_roots(
3549 SUBTREE_ABOVE_HIGHEST_SCANNED,
3550 vec![SubtreeRoot {
3551 root_hash: note_commitment(SCANNED_CHAIN).to_bytes().to_vec(),
3552 ..Default::default()
3553 }],
3554 &mut shard_trees.orchard,
3555 )
3556 .unwrap();
3557 let mut wallet = wallet(shard_trees);
3558
3559 rollback_unscanned_tree_states(&mut wallet).unwrap();
3560
3561 assert_eq!(
3562 witness::stored_subtree_root_count(&wallet.get_shard_trees_mut().unwrap().orchard),
3563 SUBTREE_ABOVE_HIGHEST_SCANNED + 1
3564 );
3565 }
3566
3567 #[test]
3572 fn shard_tree_that_cannot_roll_back_is_left_as_it_is() {
3573 let mut shard_trees = ShardTrees::new();
3574 for height in [HIGHEST_SCANNED, NEXT_BLOCK] {
3575 shard_trees
3576 .orchard
3577 .store_mut()
3578 .add_checkpoint(
3579 BlockHeight::from_u32(height),
3580 Checkpoint::at_position(Position::from(NEXT_BLOCK_POSITION - 1)),
3581 )
3582 .unwrap();
3583 }
3584 let mut wallet = wallet(shard_trees);
3585
3586 rollback_unscanned_tree_states(&mut wallet).unwrap();
3587
3588 let shard_trees = wallet.get_shard_trees_mut().unwrap();
3589 assert_eq!(
3590 shard_trees.orchard.store().max_checkpoint_id().unwrap(),
3591 Some(BlockHeight::from_u32(NEXT_BLOCK))
3592 );
3593 }
3594 }
3595
3596 mod sync_status_pool_accounting {
3605 use std::collections::BTreeMap;
3606
3607 use zcash_primitives::block::BlockHash;
3608 use zcash_protocol::consensus::BlockHeight;
3609
3610 use crate::mocks::MockWalletBuilder;
3611 use crate::sync::{ScanPriority, ScanRange, sync_status};
3612 use crate::wallet::{SyncState, TreeBounds, WalletBlock};
3613
3614 fn block(height: u32, tree_bounds: TreeBounds) -> WalletBlock {
3617 WalletBlock {
3618 block_height: BlockHeight::from_u32(height),
3619 block_hash: BlockHash([0; 32]),
3620 prev_hash: BlockHash([0; 32]),
3621 time: 0,
3622 txids: Vec::new(),
3623 tree_bounds,
3624 }
3625 }
3626
3627 fn flat_bounds(sapling: u32, orchard: u32, ironwood: u32) -> TreeBounds {
3630 TreeBounds {
3631 sapling_initial_tree_size: sapling,
3632 sapling_final_tree_size: sapling,
3633 orchard_initial_tree_size: orchard,
3634 orchard_final_tree_size: orchard,
3635 ironwood_initial_tree_size: ironwood,
3636 ironwood_final_tree_size: ironwood,
3637 }
3638 }
3639
3640 #[tokio::test]
3641 async fn exact_ratio_fields_agree_with_per_pool_totals() {
3642 let mut sync_state = SyncState::new_for_test(vec![ScanRange::from_parts(
3646 BlockHeight::from_u32(1_000)..BlockHeight::from_u32(1_010),
3647 ScanPriority::Scanned,
3648 )]);
3649 sync_state.initial_sync_state.sync_start_height = BlockHeight::from_u32(1_000);
3650 sync_state.initial_sync_state.wallet_tree_bounds = TreeBounds {
3653 sapling_initial_tree_size: 100,
3654 sapling_final_tree_size: 110,
3655 orchard_initial_tree_size: 200,
3656 orchard_final_tree_size: 220,
3657 ironwood_initial_tree_size: 300,
3658 ironwood_final_tree_size: 340,
3659 };
3660 sync_state
3661 .initial_sync_state
3662 .previously_scanned_sapling_outputs = 1;
3663 sync_state
3664 .initial_sync_state
3665 .previously_scanned_orchard_outputs = 2;
3666 sync_state
3667 .initial_sync_state
3668 .previously_scanned_ironwood_outputs = 3;
3669
3670 let wallet_blocks = BTreeMap::from([
3671 (
3672 BlockHeight::from_u32(1_000),
3673 block(1_000, flat_bounds(100, 200, 300)),
3674 ),
3675 (
3676 BlockHeight::from_u32(1_009),
3677 block(1_009, flat_bounds(103, 205, 307)),
3678 ),
3679 ]);
3680
3681 let wallet = MockWalletBuilder::new()
3682 .sync_state(sync_state)
3683 .wallet_blocks(wallet_blocks)
3684 .create_mock_wallet();
3685
3686 let status = sync_status(&wallet).await.unwrap();
3687
3688 assert_eq!(status.total_sapling_outputs_scanned, 3);
3689 assert_eq!(status.total_orchard_outputs_scanned, 5);
3690 assert_eq!(status.total_ironwood_outputs_scanned, 7);
3691
3692 assert_eq!(
3697 status.total_outputs_scanned,
3698 u64::from(status.total_sapling_outputs_scanned)
3699 + u64::from(status.total_orchard_outputs_scanned)
3700 + u64::from(status.total_ironwood_outputs_scanned),
3701 );
3702 assert_eq!(status.total_outputs_scanned, 15);
3703 assert_eq!(status.total_outputs, 70);
3704 }
3705
3706 #[tokio::test]
3711 async fn status_survives_a_baseline_above_the_span() {
3712 let mut sync_state = SyncState::new_for_test(vec![ScanRange::from_parts(
3713 BlockHeight::from_u32(1_000)..BlockHeight::from_u32(1_010),
3714 ScanPriority::Scanned,
3715 )]);
3716 sync_state.initial_sync_state.sync_start_height = BlockHeight::from_u32(1_000);
3717 sync_state.initial_sync_state.wallet_tree_bounds = TreeBounds {
3719 sapling_initial_tree_size: 100,
3720 sapling_final_tree_size: 100,
3721 orchard_initial_tree_size: 200,
3722 orchard_final_tree_size: 210,
3723 ironwood_initial_tree_size: 300,
3724 ironwood_final_tree_size: 300,
3725 };
3726 sync_state
3728 .initial_sync_state
3729 .previously_scanned_orchard_outputs = 70;
3730
3731 let wallet_blocks = BTreeMap::from([
3732 (
3733 BlockHeight::from_u32(1_000),
3734 block(1_000, flat_bounds(100, 200, 300)),
3735 ),
3736 (
3737 BlockHeight::from_u32(1_009),
3738 block(1_009, flat_bounds(100, 275, 300)),
3739 ),
3740 ]);
3741 let wallet = MockWalletBuilder::new()
3742 .sync_state(sync_state)
3743 .wallet_blocks(wallet_blocks)
3744 .create_mock_wallet();
3745
3746 let status = sync_status(&wallet)
3747 .await
3748 .expect("an inconsistent state is a reading, not a failure");
3749
3750 assert_eq!(status.total_outputs_scanned, 75);
3752 assert_eq!(status.total_outputs, 10);
3753 assert_eq!(status.session_orchard_outputs_scanned, 5);
3755 }
3756 }
3757
3758 mod sync_status_stale_initial_state {
3764 use std::collections::BTreeMap;
3765
3766 use zcash_primitives::block::BlockHash;
3767 use zcash_protocol::consensus::BlockHeight;
3768
3769 use crate::mocks::MockWalletBuilder;
3770 use crate::sync::{ScanPriority, ScanRange, sync_status};
3771 use crate::wallet::{SyncState, TreeBounds, WalletBlock};
3772
3773 const BIRTHDAY_HEIGHT: u32 = 1_000;
3775 const TOTAL_BLOCKS: u32 = 10;
3777 const SCANNED_BLOCKS: u32 = 5;
3779 const CHAIN_TIP_HEIGHT: u32 = BIRTHDAY_HEIGHT + TOTAL_BLOCKS - 1;
3781 const SCANNED_RANGE_END: u32 = BIRTHDAY_HEIGHT + SCANNED_BLOCKS;
3783 const STALENESS_FACTOR: u32 = 500;
3785 const STALE_PREVIOUSLY_SCANNED_BLOCKS: u32 = TOTAL_BLOCKS * STALENESS_FACTOR;
3787
3788 const SAPLING_INITIAL_TREE_SIZE: u32 = 100;
3790 const SAPLING_TOTAL_OUTPUTS: u32 = 10;
3792 const SAPLING_SCANNED_OUTPUTS: u32 = 3;
3794 const ORCHARD_INITIAL_TREE_SIZE: u32 = 200;
3796 const ORCHARD_TOTAL_OUTPUTS: u32 = 20;
3798 const ORCHARD_SCANNED_OUTPUTS: u32 = 5;
3800 const IRONWOOD_INITIAL_TREE_SIZE: u32 = 300;
3802 const IRONWOOD_TOTAL_OUTPUTS: u32 = 40;
3804 const IRONWOOD_SCANNED_OUTPUTS: u32 = 7;
3806 const TOTAL_OUTPUTS: u32 =
3808 SAPLING_TOTAL_OUTPUTS + ORCHARD_TOTAL_OUTPUTS + IRONWOOD_TOTAL_OUTPUTS;
3809 const SCANNED_OUTPUTS: u32 =
3811 SAPLING_SCANNED_OUTPUTS + ORCHARD_SCANNED_OUTPUTS + IRONWOOD_SCANNED_OUTPUTS;
3812 const STALE_PREVIOUSLY_SCANNED_OUTPUTS: u32 = TOTAL_OUTPUTS * STALENESS_FACTOR;
3814
3815 fn block(height: u32, tree_bounds: TreeBounds) -> WalletBlock {
3818 WalletBlock {
3819 block_height: BlockHeight::from_u32(height),
3820 block_hash: BlockHash([0; 32]),
3821 prev_hash: BlockHash([0; 32]),
3822 time: 0,
3823 txids: Vec::new(),
3824 tree_bounds,
3825 }
3826 }
3827
3828 fn flat_bounds(sapling: u32, orchard: u32, ironwood: u32) -> TreeBounds {
3831 TreeBounds {
3832 sapling_initial_tree_size: sapling,
3833 sapling_final_tree_size: sapling,
3834 orchard_initial_tree_size: orchard,
3835 orchard_final_tree_size: orchard,
3836 ironwood_initial_tree_size: ironwood,
3837 ironwood_final_tree_size: ironwood,
3838 }
3839 }
3840
3841 fn stale_state_wallet() -> crate::mocks::MockWallet {
3846 let mut sync_state = SyncState::new_for_test(vec![
3847 ScanRange::from_parts(
3848 BlockHeight::from_u32(BIRTHDAY_HEIGHT)
3849 ..BlockHeight::from_u32(SCANNED_RANGE_END),
3850 ScanPriority::Scanned,
3851 ),
3852 ScanRange::from_parts(
3853 BlockHeight::from_u32(SCANNED_RANGE_END)
3854 ..BlockHeight::from_u32(CHAIN_TIP_HEIGHT + 1),
3855 ScanPriority::Historic,
3856 ),
3857 ]);
3858 sync_state.initial_sync_state.sync_start_height =
3859 BlockHeight::from_u32(BIRTHDAY_HEIGHT);
3860 sync_state.initial_sync_state.wallet_tree_bounds = TreeBounds {
3861 sapling_initial_tree_size: SAPLING_INITIAL_TREE_SIZE,
3862 sapling_final_tree_size: SAPLING_INITIAL_TREE_SIZE + SAPLING_TOTAL_OUTPUTS,
3863 orchard_initial_tree_size: ORCHARD_INITIAL_TREE_SIZE,
3864 orchard_final_tree_size: ORCHARD_INITIAL_TREE_SIZE + ORCHARD_TOTAL_OUTPUTS,
3865 ironwood_initial_tree_size: IRONWOOD_INITIAL_TREE_SIZE,
3866 ironwood_final_tree_size: IRONWOOD_INITIAL_TREE_SIZE + IRONWOOD_TOTAL_OUTPUTS,
3867 };
3868 sync_state.initial_sync_state.previously_scanned_blocks =
3869 STALE_PREVIOUSLY_SCANNED_BLOCKS;
3870 sync_state
3871 .initial_sync_state
3872 .previously_scanned_sapling_outputs = STALE_PREVIOUSLY_SCANNED_OUTPUTS;
3873 sync_state
3874 .initial_sync_state
3875 .previously_scanned_orchard_outputs = STALE_PREVIOUSLY_SCANNED_OUTPUTS;
3876 sync_state
3877 .initial_sync_state
3878 .previously_scanned_ironwood_outputs = STALE_PREVIOUSLY_SCANNED_OUTPUTS;
3879
3880 let wallet_blocks = BTreeMap::from([
3881 (
3882 BlockHeight::from_u32(BIRTHDAY_HEIGHT),
3883 block(
3884 BIRTHDAY_HEIGHT,
3885 flat_bounds(
3886 SAPLING_INITIAL_TREE_SIZE,
3887 ORCHARD_INITIAL_TREE_SIZE,
3888 IRONWOOD_INITIAL_TREE_SIZE,
3889 ),
3890 ),
3891 ),
3892 (
3893 BlockHeight::from_u32(SCANNED_RANGE_END - 1),
3894 block(
3895 SCANNED_RANGE_END - 1,
3896 flat_bounds(
3897 SAPLING_INITIAL_TREE_SIZE + SAPLING_SCANNED_OUTPUTS,
3898 ORCHARD_INITIAL_TREE_SIZE + ORCHARD_SCANNED_OUTPUTS,
3899 IRONWOOD_INITIAL_TREE_SIZE + IRONWOOD_SCANNED_OUTPUTS,
3900 ),
3901 ),
3902 ),
3903 ]);
3904
3905 MockWalletBuilder::new()
3906 .sync_state(sync_state)
3907 .wallet_blocks(wallet_blocks)
3908 .create_mock_wallet()
3909 }
3910
3911 #[tokio::test]
3916 async fn stale_block_state_reports_total_progress() {
3917 let wallet = stale_state_wallet();
3918
3919 let status = sync_status(&wallet).await.unwrap();
3920
3921 assert_eq!(status.total_blocks_scanned, SCANNED_BLOCKS);
3922 assert_eq!(status.total_blocks, TOTAL_BLOCKS);
3923 }
3924
3925 #[tokio::test]
3930 async fn stale_output_state_reports_total_progress() {
3931 let wallet = stale_state_wallet();
3932
3933 let status = sync_status(&wallet).await.unwrap();
3934
3935 assert_eq!(status.total_outputs_scanned, u64::from(SCANNED_OUTPUTS));
3936 assert_eq!(status.total_outputs, u64::from(TOTAL_OUTPUTS));
3937 }
3938 }
3939
3940 mod on_completion {
3942 use std::sync::atomic::AtomicU8;
3943
3944 use crate::wallet::SyncMode;
3945
3946 #[test]
3947 fn running_sync_is_shutdown() {
3948 assert_eq!(SyncMode::Running.on_completion(), SyncMode::Shutdown);
3949 }
3950
3951 #[test]
3952 fn paused_sync_stays_paused() {
3953 assert_eq!(SyncMode::Paused.on_completion(), SyncMode::Paused);
3954 }
3955
3956 #[test]
3957 fn shutdown_requested_by_the_consumer_is_kept() {
3958 assert_eq!(SyncMode::Shutdown.on_completion(), SyncMode::Shutdown);
3959 }
3960
3961 #[test]
3962 fn sync_that_is_not_running_is_left_alone() {
3963 assert_eq!(SyncMode::NotRunning.on_completion(), SyncMode::NotRunning);
3964 }
3965
3966 #[test]
3967 fn apply_stores_the_step_and_returns_the_mode_it_replaced() {
3968 let sync_mode = AtomicU8::new(SyncMode::Running as u8);
3969
3970 let before = SyncMode::apply(&sync_mode, SyncMode::on_completion).unwrap();
3971
3972 assert_eq!(before, SyncMode::Running);
3973 assert_eq!(
3974 SyncMode::from_atomic_u8(&sync_mode).unwrap(),
3975 SyncMode::Shutdown
3976 );
3977 }
3978
3979 #[test]
3980 fn transition_leaves_another_mode_in_place_and_reports_it() {
3981 let sync_mode = AtomicU8::new(SyncMode::Paused as u8);
3982
3983 let before =
3984 SyncMode::transition(&sync_mode, SyncMode::Running, SyncMode::Shutdown).unwrap();
3985
3986 assert_eq!(before, SyncMode::Paused);
3987 assert_eq!(
3988 SyncMode::from_atomic_u8(&sync_mode).unwrap(),
3989 SyncMode::Paused
3990 );
3991 }
3992 }
3993
3994 mod drain_verdict {
3997 use std::{
3998 sync::{
3999 Arc,
4000 atomic::{AtomicBool, AtomicU32},
4001 },
4002 time::Duration,
4003 };
4004
4005 use zingo_netutils::time::MEMPOOL_DRAIN_CEILING;
4006
4007 use crate::sync::{MempoolDrainVerdict, mempool_drain_verdict};
4008
4009 type DrainCase = (
4012 Arc<AtomicBool>,
4013 Arc<AtomicU32>,
4014 Duration,
4015 Option<Duration>,
4016 MempoolDrainVerdict,
4017 &'static str,
4018 );
4019
4020 #[tokio::test]
4021 async fn table() {
4022 let cases: &[DrainCase] = &[
4023 (
4027 Arc::new(AtomicBool::new(false)),
4028 Arc::new(AtomicU32::new(0)),
4029 Duration::from_millis(0),
4030 None,
4031 MempoolDrainVerdict::NotShutdown,
4032 "no stream yet",
4033 ),
4034 (
4036 Arc::new(AtomicBool::new(false)),
4037 Arc::new(AtomicU32::new(0)),
4038 Duration::from_millis(100),
4039 Some(Duration::from_millis(50)),
4040 MempoolDrainVerdict::ShutdownAndDrainComplete,
4041 "settling",
4042 ),
4043 (
4046 Arc::new(AtomicBool::new(false)),
4047 Arc::new(AtomicU32::new(0)),
4048 Duration::from_millis(0),
4049 Some(Duration::from_millis(1_400)),
4050 MempoolDrainVerdict::ShutdownAndDrainComplete,
4051 "settled and drained",
4052 ),
4053 (
4057 Arc::new(AtomicBool::new(false)),
4058 Arc::new(AtomicU32::new(3)),
4059 Duration::from_millis(3_000),
4060 Some(Duration::from_millis(1_400)),
4061 MempoolDrainVerdict::ShutdownNotDrained,
4062 "unprocessed work",
4063 ),
4064 (
4071 Arc::new(AtomicBool::new(false)),
4072 Arc::new(AtomicU32::new(0)),
4073 MEMPOOL_DRAIN_CEILING,
4074 None,
4075 MempoolDrainVerdict::ShutdownAndDrainComplete,
4076 "ceiling without stream",
4077 ),
4078 ];
4079 for (shutdown_mempool, unprocessed, connected_ms, elapsed_ms, expected, name) in cases {
4080 assert_eq!(
4081 mempool_drain_verdict(
4082 shutdown_mempool.clone(),
4083 unprocessed.clone(),
4084 *connected_ms,
4085 *elapsed_ms,
4086 )
4087 .await,
4088 *expected,
4089 "{name}"
4090 );
4091
4092 }
4094 }
4095 }
4096
4097 mod spend_reset_lifecycle {
4106 use std::collections::HashMap;
4107
4108 use sapling_crypto::value::NoteValue;
4109 use zcash_keys::keys::UnifiedSpendingKey;
4110 use zcash_primitives::transaction::TxId;
4111 use zcash_protocol::{
4112 consensus::{BlockHeight, MAIN_NETWORK},
4113 memo::Memo,
4114 };
4115 use zingo_status::confirmation_status::ConfirmationStatus;
4116
4117 use crate::{
4118 mocks::{MockWallet, MockWalletBuilder},
4119 sync::{set_transactions_failed, spend},
4120 wallet::{
4121 NullifierMap, OutputId, ScanTarget, WalletNote, WalletTransaction,
4122 traits::{SyncNullifiers, SyncTransactions},
4123 },
4124 };
4125
4126 pub(super) const FUNDING_HEIGHT: BlockHeight = BlockHeight::from_u32(10);
4127 pub(super) const SPEND_HEIGHT: BlockHeight = BlockHeight::from_u32(100);
4128 pub(super) const FUNDING_TXID: TxId = TxId::from_bytes([1; 32]);
4129 pub(super) const SPENDING_TXID: TxId = TxId::from_bytes([2; 32]);
4130 pub(super) const NOTE_NULLIFIER: sapling_crypto::Nullifier =
4131 sapling_crypto::Nullifier([42; 32]);
4132
4133 fn spending_record(status: ConfirmationStatus) -> WalletTransaction {
4135 WalletTransaction::new_for_test(SPENDING_TXID, status)
4136 }
4137
4138 pub(super) fn spending_key() -> UnifiedSpendingKey {
4146 UnifiedSpendingKey::from_seed(&MAIN_NETWORK, &[0; 32], zip32::AccountId::ZERO).unwrap()
4147 }
4148
4149 pub(super) fn funding_transaction(spending_transaction: Option<TxId>) -> WalletTransaction {
4150 let (_, recipient) = spending_key().sapling().default_address();
4151 let crypto_note = sapling_crypto::Note::from_parts(
4152 recipient,
4153 NoteValue::from_raw(100_000),
4154 sapling_crypto::Rseed::AfterZip212([0; 32]),
4155 );
4156 let mut note = WalletNote::new_for_test(
4157 OutputId::new(FUNDING_TXID, 0),
4158 zip32::AccountId::ZERO,
4159 zip32::Scope::External,
4160 crypto_note,
4161 Memo::Empty,
4162 None,
4163 )
4164 .with_nullifier_for_test(NOTE_NULLIFIER);
4165 note.spending_transaction = spending_transaction;
4166
4167 WalletTransaction::new_for_test(
4168 FUNDING_TXID,
4169 ConfirmationStatus::Confirmed(FUNDING_HEIGHT),
4170 )
4171 .with_sapling_notes_for_test(vec![note])
4172 }
4173
4174 fn get_spending_txid(wallet: &MockWallet) -> Option<TxId> {
4175 wallet
4176 .get_wallet_transactions()
4177 .unwrap()
4178 .get(&FUNDING_TXID)
4179 .unwrap()
4180 .sapling_notes()
4181 .first()
4182 .unwrap()
4183 .spending_transaction
4184 }
4185
4186 fn run_spend_detection(wallet: &mut MockWallet) {
4190 let (sapling_nullifiers, orchard_nullifiers, ironwood_nullifiers) =
4191 spend::collect_derived_nullifiers(
4192 wallet.get_wallet_transactions().unwrap().values(),
4193 );
4194 let spend_scan_targets = spend::detect_shielded_spends(
4195 wallet.get_nullifiers().unwrap(),
4196 &sapling_nullifiers,
4197 &orchard_nullifiers,
4198 &ironwood_nullifiers,
4199 );
4200 spend::update_spent_notes(wallet, spend_scan_targets, true).unwrap();
4201 }
4202
4203 #[test]
4207 fn reset_before_scan_heals_on_spend_detection() {
4208 let mut wallet_transactions = HashMap::new();
4209 wallet_transactions.insert(FUNDING_TXID, funding_transaction(None));
4210 wallet_transactions.insert(
4211 SPENDING_TXID,
4212 spending_record(ConfirmationStatus::Failed(SPEND_HEIGHT)),
4213 );
4214 let mut nullifier_map = NullifierMap::new();
4215 nullifier_map.sapling.insert(
4216 NOTE_NULLIFIER,
4217 ScanTarget::internal(SPEND_HEIGHT, SPENDING_TXID),
4218 );
4219 let mut wallet = MockWalletBuilder::new()
4220 .wallet_transactions(wallet_transactions)
4221 .nullifier_map(nullifier_map)
4222 .create_mock_wallet();
4223
4224 run_spend_detection(&mut wallet);
4225
4226 assert_eq!(get_spending_txid(&wallet), Some(SPENDING_TXID));
4227 assert!(
4230 wallet
4231 .get_nullifiers()
4232 .unwrap()
4233 .sapling
4234 .contains_key(&NOTE_NULLIFIER)
4235 );
4236 }
4237
4238 #[test]
4260 fn failing_a_confirmed_transaction_must_not_destroy_the_spend_observation() {
4261 let mut wallet_transactions = HashMap::new();
4262 wallet_transactions.insert(FUNDING_TXID, funding_transaction(Some(SPENDING_TXID)));
4263 wallet_transactions.insert(
4264 SPENDING_TXID,
4265 spending_record(ConfirmationStatus::Confirmed(SPEND_HEIGHT)),
4266 );
4267 let mut wallet = MockWalletBuilder::new()
4269 .wallet_transactions(wallet_transactions)
4270 .create_mock_wallet();
4271
4272 set_transactions_failed(
4275 wallet.get_wallet_transactions_mut().unwrap(),
4276 vec![SPENDING_TXID],
4277 );
4278
4279 assert_eq!(
4281 wallet
4282 .get_wallet_transactions()
4283 .unwrap()
4284 .get(&SPENDING_TXID)
4285 .unwrap()
4286 .status(),
4287 ConfirmationStatus::Confirmed(SPEND_HEIGHT)
4288 );
4289
4290 run_spend_detection(&mut wallet);
4293 assert_eq!(
4294 get_spending_txid(&wallet),
4295 Some(SPENDING_TXID),
4296 "the on-chain spend observation was destroyed: \
4297 the note is now a permanent phantom unspent note"
4298 );
4299 }
4300
4301 #[test]
4305 fn scanned_transaction_overwrites_failed_record() {
4306 let mut wallet_transactions = HashMap::new();
4307 wallet_transactions.insert(
4308 SPENDING_TXID,
4309 spending_record(ConfirmationStatus::Failed(SPEND_HEIGHT)),
4310 );
4311 let mut wallet = MockWalletBuilder::new()
4312 .wallet_transactions(wallet_transactions)
4313 .create_mock_wallet();
4314
4315 wallet
4316 .extend_wallet_transactions(HashMap::from([(
4317 SPENDING_TXID,
4318 spending_record(ConfirmationStatus::Confirmed(SPEND_HEIGHT)),
4319 )]))
4320 .unwrap();
4321
4322 assert_eq!(
4323 wallet
4324 .get_wallet_transactions()
4325 .unwrap()
4326 .get(&SPENDING_TXID)
4327 .unwrap()
4328 .status(),
4329 ConfirmationStatus::Confirmed(SPEND_HEIGHT)
4330 );
4331 }
4332 }
4333
4334 mod transparent_spend_without_change {
4341 use std::collections::{BTreeMap, HashMap};
4342
4343 use tokio::sync::mpsc;
4344 use zcash_primitives::{block::BlockHash, transaction::TxId};
4345 use zcash_protocol::{consensus::BlockHeight, value::Zatoshis};
4346 use zcash_transparent::{address::Script, keys::NonHardenedChildIndex};
4347 use zingo_netutils::lightwallet_protocol::RawTransaction;
4348 use zingo_status::confirmation_status::ConfirmationStatus;
4349
4350 use crate::{
4351 client::FetchRequest,
4352 error::SyncError,
4353 keys::transparent::{TransparentAddressId, TransparentScope},
4354 mocks::{MockWallet, MockWalletBuilder, MockWalletError},
4355 sync::spend,
4356 wallet::{
4357 OutputId, ScanTarget, TransparentCoin, TreeBounds, WalletBlock, WalletTransaction,
4358 traits::{SyncOutPoints, SyncTransactions},
4359 },
4360 };
4361
4362 use super::NETWORK;
4363 const FUNDING_HEIGHT: BlockHeight = BlockHeight::from_u32(10);
4364 const SPEND_HEIGHT: BlockHeight = BlockHeight::from_u32(100);
4365 const FUNDING_TXID: TxId = TxId::from_bytes([1; 32]);
4366
4367 fn spending_record(status: ConfirmationStatus) -> WalletTransaction {
4370 let txid = WalletTransaction::new_for_test(TxId::from_bytes([0; 32]), status)
4371 .transaction()
4372 .txid();
4373 WalletTransaction::new_for_test(txid, status)
4374 }
4375
4376 fn wallet_with_mapped_spend(spending_record: WalletTransaction) -> MockWallet {
4379 let coin_id = OutputId::new(FUNDING_TXID, 0);
4380 let coin = TransparentCoin {
4381 output_id: coin_id,
4382 key_id: TransparentAddressId::new(
4383 zip32::AccountId::ZERO,
4384 TransparentScope::External,
4385 NonHardenedChildIndex::ZERO,
4386 ),
4387 address: String::new(),
4388 script: Script::default(),
4389 value: Zatoshis::const_from_u64(100_000),
4390 spending_transaction: None,
4391 };
4392 let funding_transaction = WalletTransaction::new_for_test(
4393 FUNDING_TXID,
4394 ConfirmationStatus::Confirmed(FUNDING_HEIGHT),
4395 )
4396 .with_transparent_coins_for_test(vec![coin]);
4397 let spend_scan_target = ScanTarget::internal(SPEND_HEIGHT, spending_record.txid());
4398
4399 MockWalletBuilder::new()
4400 .wallet_transactions(HashMap::from([
4401 (FUNDING_TXID, funding_transaction),
4402 (spending_record.txid(), spending_record),
4403 ]))
4404 .outpoint_map(BTreeMap::from([(coin_id, spend_scan_target)]))
4405 .create_mock_wallet()
4406 }
4407
4408 fn scanned_blocks() -> BTreeMap<BlockHeight, WalletBlock> {
4409 BTreeMap::from([(
4410 SPEND_HEIGHT,
4411 WalletBlock {
4412 block_height: SPEND_HEIGHT,
4413 block_hash: BlockHash([0; 32]),
4414 prev_hash: BlockHash([0; 32]),
4415 time: 0,
4416 txids: Vec::new(),
4417 tree_bounds: TreeBounds {
4418 sapling_initial_tree_size: 0,
4419 sapling_final_tree_size: 0,
4420 orchard_initial_tree_size: 0,
4421 orchard_final_tree_size: 0,
4422 ironwood_initial_tree_size: 0,
4423 ironwood_final_tree_size: 0,
4424 },
4425 },
4426 )])
4427 }
4428
4429 fn spawn_fetcher(transaction: &WalletTransaction) -> mpsc::UnboundedSender<FetchRequest> {
4431 let mut data = Vec::new();
4432 transaction.transaction().write(&mut data).unwrap();
4433 let (fetch_request_sender, mut fetch_request_receiver) = mpsc::unbounded_channel();
4434 tokio::spawn(async move {
4435 while let Some(fetch_request) = fetch_request_receiver.recv().await {
4436 match fetch_request {
4437 FetchRequest::Transaction(reply_sender, _txid) => {
4438 let _ignore_error = reply_sender.send(Ok(RawTransaction {
4439 data: data.clone(),
4440 height: u64::from(SPEND_HEIGHT),
4441 }));
4442 }
4443 _ => panic!("unexpected fetch request"),
4444 }
4445 }
4446 });
4447
4448 fetch_request_sender
4449 }
4450
4451 fn get_coin_spending_txid(wallet: &MockWallet) -> Option<TxId> {
4452 wallet
4453 .get_wallet_transactions()
4454 .unwrap()
4455 .get(&FUNDING_TXID)
4456 .unwrap()
4457 .transparent_coins()
4458 .first()
4459 .unwrap()
4460 .spending_transaction
4461 }
4462
4463 async fn update_transparent_spends(
4466 wallet: &mut MockWallet,
4467 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
4468 ) -> Result<(), SyncError<MockWalletError>> {
4469 let mut scanned_transactions = HashMap::new();
4470 let transparent_spend_scan_targets = spend::locate_transparent_spends(
4471 &NETWORK,
4472 &*wallet,
4473 fetch_request_sender,
4474 &HashMap::new(),
4475 &scanned_blocks(),
4476 &mut scanned_transactions,
4477 &BTreeMap::new(),
4478 )
4479 .await?;
4480 wallet
4481 .extend_wallet_transactions(scanned_transactions)
4482 .unwrap();
4483 spend::apply_transparent_spends(wallet, transparent_spend_scan_targets).unwrap();
4484
4485 Ok(())
4486 }
4487
4488 #[tokio::test]
4489 async fn pending_spending_transaction_is_fetched_and_confirmed() {
4490 let spending_record = spending_record(ConfirmationStatus::Mempool(SPEND_HEIGHT));
4491 let spending_txid = spending_record.txid();
4492 let fetch_request_sender = spawn_fetcher(&spending_record);
4493 let mut wallet = wallet_with_mapped_spend(spending_record);
4494
4495 update_transparent_spends(&mut wallet, fetch_request_sender)
4496 .await
4497 .unwrap();
4498
4499 assert_eq!(
4500 wallet
4501 .get_wallet_transactions()
4502 .unwrap()
4503 .get(&spending_txid)
4504 .unwrap()
4505 .status(),
4506 ConfirmationStatus::Confirmed(SPEND_HEIGHT)
4507 );
4508 assert_eq!(get_coin_spending_txid(&wallet), Some(spending_txid));
4509 assert!(
4511 wallet
4512 .get_outpoints()
4513 .unwrap()
4514 .contains_key(&OutputId::new(FUNDING_TXID, 0))
4515 );
4516 }
4517
4518 #[tokio::test]
4521 async fn confirmed_spending_transaction_is_not_fetched() {
4522 let spending_record = spending_record(ConfirmationStatus::Confirmed(SPEND_HEIGHT));
4523 let spending_txid = spending_record.txid();
4524 let mut wallet = wallet_with_mapped_spend(spending_record);
4525 let (fetch_request_sender, fetch_request_receiver) = mpsc::unbounded_channel();
4526 drop(fetch_request_receiver);
4528
4529 update_transparent_spends(&mut wallet, fetch_request_sender)
4530 .await
4531 .unwrap();
4532
4533 assert_eq!(get_coin_spending_txid(&wallet), Some(spending_txid));
4534 }
4535
4536 #[tokio::test]
4539 async fn failed_fetch_keeps_the_mapped_spend() {
4540 let spending_record = spending_record(ConfirmationStatus::Mempool(SPEND_HEIGHT));
4541 let mut wallet = wallet_with_mapped_spend(spending_record);
4542 let (fetch_request_sender, fetch_request_receiver) = mpsc::unbounded_channel();
4543 drop(fetch_request_receiver);
4544
4545 let result = update_transparent_spends(&mut wallet, fetch_request_sender).await;
4546
4547 assert!(result.is_err());
4548 assert!(
4549 wallet
4550 .get_outpoints_mut()
4551 .unwrap()
4552 .contains_key(&OutputId::new(FUNDING_TXID, 0)),
4553 "the spend must stay mapped until it is recorded on the coin"
4554 );
4555 assert_eq!(get_coin_spending_txid(&wallet), None);
4556 }
4557 }
4558
4559 mod checked_height_validation {
4560 use zcash_protocol::consensus::BlockHeight;
4561 use zcash_protocol::local_consensus::LocalNetwork;
4562 const LOCAL_NETWORK: LocalNetwork = LocalNetwork {
4564 overwinter: Some(BlockHeight::from_u32(1)),
4565 ..super::activated_at(3)
4566 };
4567 use crate::{error::SyncError, mocks::MockWalletError, sync::checked_wallet_height};
4568 #[tokio::test]
4571 async fn get_sync_state_error() {
4572 let builder = crate::mocks::MockWalletBuilder::new();
4573 let test_error = "get_sync_state_error";
4574 let mut test_wallet = builder
4575 .get_sync_state_patch(Box::new(|_| {
4576 Err(MockWalletError::AnErrorVariant(test_error.to_string()))
4577 }))
4578 .create_mock_wallet();
4579 let res =
4580 checked_wallet_height(&mut test_wallet, BlockHeight::from_u32(1), &LOCAL_NETWORK);
4581 assert!(matches!(
4582 res,
4583 Err(SyncError::WalletError(
4584 crate::mocks::MockWalletError::AnErrorVariant(ref s)
4585 )) if s == test_error
4586 ));
4587 }
4588
4589 mod last_known_chain_height {
4590 use crate::{
4591 sync::{MAX_REORG_ALLOWANCE, ScanRange},
4592 wallet::{SyncState, traits::SyncWallet as _},
4593 };
4594 const DEFAULT_START_HEIGHT: BlockHeight = BlockHeight::from_u32(1);
4595 const _DEFAULT_LAST_KNOWN_HEIGHT: BlockHeight = BlockHeight::from_u32(102);
4596 const DEFAULT_CHAIN_HEIGHT: BlockHeight = BlockHeight::from_u32(110);
4597
4598 use super::*;
4599 #[tokio::test]
4600 async fn above_allowance() {
4601 const LAST_KNOWN_HEIGHT: BlockHeight = BlockHeight::from_u32(211);
4602 let lkch = vec![ScanRange::from_parts(
4603 DEFAULT_START_HEIGHT..LAST_KNOWN_HEIGHT,
4604 crate::sync::ScanPriority::Scanned,
4605 )];
4606 let state = SyncState {
4607 scan_ranges: lkch,
4608 ..Default::default()
4609 };
4610 let builder = crate::mocks::MockWalletBuilder::new();
4611 let mut test_wallet = builder.sync_state(state).create_mock_wallet();
4612 let res =
4613 checked_wallet_height(&mut test_wallet, DEFAULT_CHAIN_HEIGHT, &LOCAL_NETWORK);
4614 if let Err(e) = res {
4615 assert_eq!(
4616 e.to_string(),
4617 format!(
4618 "wallet height {} is more than {} blocks ahead of best chain height {}",
4619 LAST_KNOWN_HEIGHT - 1,
4620 MAX_REORG_ALLOWANCE,
4621 DEFAULT_CHAIN_HEIGHT
4622 )
4623 );
4624 } else {
4625 panic!()
4626 }
4627 }
4628 #[tokio::test]
4629 async fn above_chain_height_below_allowance() {
4630 let lkch = vec![ScanRange::from_parts(
4634 BlockHeight::from_u32(6)..BlockHeight::from_u32(10),
4635 crate::sync::ScanPriority::Scanned,
4636 )];
4637 let state = SyncState {
4638 scan_ranges: lkch,
4639 ..Default::default()
4640 };
4641 let builder = crate::mocks::MockWalletBuilder::new();
4642 let mut test_wallet = builder.sync_state(state).create_mock_wallet();
4643 let chain_height = BlockHeight::from_u32(4);
4644 let res = checked_wallet_height(&mut test_wallet, chain_height, &LOCAL_NETWORK);
4649 assert_eq!(res.unwrap(), BlockHeight::from_u32(4));
4650 }
4651 #[tokio::test]
4657 async fn above_chain_height_lowers_transparent_scan_floor() {
4658 const LAST_KNOWN_HEIGHT: BlockHeight = BlockHeight::from_u32(110);
4659 const CHAIN_HEIGHT: BlockHeight = BlockHeight::from_u32(105);
4660 const FLOOR_ABOVE_CHAIN_HEIGHT: BlockHeight = BlockHeight::from_u32(108);
4661 const FLOOR_BELOW_CHAIN_HEIGHT: BlockHeight = BlockHeight::from_u32(100);
4662
4663 for (floor, expected_floor) in [
4664 (FLOOR_ABOVE_CHAIN_HEIGHT, CHAIN_HEIGHT),
4665 (CHAIN_HEIGHT, CHAIN_HEIGHT),
4666 (FLOOR_BELOW_CHAIN_HEIGHT, FLOOR_BELOW_CHAIN_HEIGHT),
4667 ] {
4668 let state = SyncState {
4669 scan_ranges: vec![ScanRange::from_parts(
4670 DEFAULT_START_HEIGHT..LAST_KNOWN_HEIGHT + 1,
4671 crate::sync::ScanPriority::Scanned,
4672 )],
4673 transparent_scan_floor: Some(floor),
4674 ..Default::default()
4675 };
4676 let mut test_wallet = crate::mocks::MockWalletBuilder::new()
4677 .sync_state(state)
4678 .create_mock_wallet();
4679
4680 let last_known_chain_height =
4681 checked_wallet_height(&mut test_wallet, CHAIN_HEIGHT, &LOCAL_NETWORK)
4682 .unwrap();
4683
4684 assert_eq!(last_known_chain_height, CHAIN_HEIGHT);
4685 assert_eq!(
4686 test_wallet.get_sync_state().unwrap().transparent_scan_floor,
4687 Some(expected_floor),
4688 "floor {floor}"
4689 );
4690 }
4691 }
4692 #[ignore = "in progress"]
4693 #[tokio::test]
4694 async fn equal_or_below_chain_height_and_above_sapling() {
4695 let lkch = vec![ScanRange::from_parts(
4696 BlockHeight::from_u32(1)..BlockHeight::from_u32(10),
4697 crate::sync::ScanPriority::Scanned,
4698 )];
4699 let state = SyncState {
4700 scan_ranges: lkch,
4701 ..Default::default()
4702 };
4703 let builder = crate::mocks::MockWalletBuilder::new();
4704 let mut _test_wallet = builder.sync_state(state).create_mock_wallet();
4705 }
4706 #[ignore = "in progress"]
4707 #[tokio::test]
4708 async fn equal_or_below_chain_height_and_below_sapling() {
4709 let lkch = vec![ScanRange::from_parts(
4712 BlockHeight::from_u32(1)..BlockHeight::from_u32(10),
4713 crate::sync::ScanPriority::Scanned,
4714 )];
4715 let state = SyncState {
4716 scan_ranges: lkch,
4717 ..Default::default()
4718 };
4719 let builder = crate::mocks::MockWalletBuilder::new();
4720 let mut _test_wallet = builder.sync_state(state).create_mock_wallet();
4721 }
4722 #[ignore = "in progress"]
4723 #[tokio::test]
4724 async fn below_sapling() {
4725 let lkch = vec![ScanRange::from_parts(
4726 BlockHeight::from_u32(1)..BlockHeight::from_u32(10),
4727 crate::sync::ScanPriority::Scanned,
4728 )];
4729 let state = SyncState {
4730 scan_ranges: lkch,
4731 ..Default::default()
4732 };
4733 let builder = crate::mocks::MockWalletBuilder::new();
4734 let mut _test_wallet = builder.sync_state(state).create_mock_wallet();
4735 }
4736 }
4737 mod no_last_known_chain_height {
4738 use super::*;
4739 #[tokio::test]
4741 async fn get_bday_error() {
4742 let test_error = "get_bday_error";
4743 let builder = crate::mocks::MockWalletBuilder::new();
4744 let mut test_wallet = builder
4745 .get_birthday_patch(Box::new(|_| {
4746 Err(crate::mocks::MockWalletError::AnErrorVariant(
4747 test_error.to_string(),
4748 ))
4749 }))
4750 .create_mock_wallet();
4751 let res = checked_wallet_height(
4752 &mut test_wallet,
4753 BlockHeight::from_u32(1),
4754 &LOCAL_NETWORK,
4755 );
4756 assert!(matches!(
4757 res,
4758 Err(SyncError::WalletError(
4759 crate::mocks::MockWalletError::AnErrorVariant(ref s)
4760 )) if s == test_error
4761 ));
4762 }
4763 #[ignore = "in progress"]
4764 #[tokio::test]
4765 async fn raw_bday_above_chain_height() {
4766 let builder = crate::mocks::MockWalletBuilder::new();
4767 let mut test_wallet = builder
4768 .birthday(BlockHeight::from_u32(15))
4769 .create_mock_wallet();
4770 let res = checked_wallet_height(
4771 &mut test_wallet,
4772 BlockHeight::from_u32(1),
4773 &LOCAL_NETWORK,
4774 );
4775 if let Err(e) = res {
4776 assert_eq!(
4777 e.to_string(),
4778 format!(
4779 "wallet height is more than {} blocks ahead of best chain height",
4780 15 - 1
4781 )
4782 );
4783 } else {
4784 panic!()
4785 }
4786 }
4787 mod sapling_height {
4788 use super::*;
4789 #[tokio::test]
4790 async fn raw_bday_above() {
4791 let builder = crate::mocks::MockWalletBuilder::new();
4792 let mut test_wallet = builder
4793 .birthday(BlockHeight::from_u32(4))
4794 .create_mock_wallet();
4795 let res = checked_wallet_height(
4796 &mut test_wallet,
4797 BlockHeight::from_u32(5),
4798 &LOCAL_NETWORK,
4799 );
4800 assert_eq!(res.unwrap(), BlockHeight::from_u32(4 - 1));
4801 }
4802 #[tokio::test]
4803 async fn raw_bday_equal() {
4804 let builder = crate::mocks::MockWalletBuilder::new();
4805 let mut test_wallet = builder
4806 .birthday(BlockHeight::from_u32(3))
4807 .create_mock_wallet();
4808 let res = checked_wallet_height(
4809 &mut test_wallet,
4810 BlockHeight::from_u32(5),
4811 &LOCAL_NETWORK,
4812 );
4813 assert_eq!(res.unwrap(), BlockHeight::from_u32(3 - 1));
4814 }
4815 #[tokio::test]
4816 async fn raw_bday_below() {
4817 let builder = crate::mocks::MockWalletBuilder::new();
4818 let mut test_wallet = builder
4819 .birthday(BlockHeight::from_u32(1))
4820 .create_mock_wallet();
4821 let res = checked_wallet_height(
4822 &mut test_wallet,
4823 BlockHeight::from_u32(5),
4824 &LOCAL_NETWORK,
4825 );
4826 assert!(matches!(res, Err(SyncError::BirthdayBelowSapling(1, 3))));
4827 }
4828 }
4829 }
4830 }
4831
4832 mod expire_transactions {
4833 use std::collections::HashMap;
4834
4835 use zcash_protocol::TxId;
4836 use zcash_protocol::consensus::BlockHeight;
4837 use zingo_status::confirmation_status::ConfirmationStatus;
4838
4839 use crate::mocks::{MockWallet, MockWalletBuilder};
4840 use crate::sync::{ScanPriority, ScanRange, expire_transactions};
4841 use crate::wallet::{SyncState, WalletTransaction};
4842
4843 const UNSCANNED_START: u32 = 51;
4844 const UNSCANNED_END: u32 = 91;
4845 const CHAIN_HEIGHT: u32 = 200;
4846
4847 fn wallet_at_height(chain_height: u32, transactions: Vec<WalletTransaction>) -> MockWallet {
4849 wallet_with_scan_ranges(
4850 vec![ScanRange::from_parts(
4851 BlockHeight::from_u32(1)..BlockHeight::from_u32(chain_height + 1),
4852 ScanPriority::Scanned,
4853 )],
4854 transactions,
4855 )
4856 }
4857
4858 fn wallet_with_unscanned_range(transactions: Vec<WalletTransaction>) -> MockWallet {
4861 wallet_with_scan_ranges(
4862 vec![
4863 ScanRange::from_parts(
4864 BlockHeight::from_u32(1)..BlockHeight::from_u32(UNSCANNED_START),
4865 ScanPriority::Scanned,
4866 ),
4867 ScanRange::from_parts(
4868 BlockHeight::from_u32(UNSCANNED_START)
4869 ..BlockHeight::from_u32(UNSCANNED_END),
4870 ScanPriority::Historic,
4871 ),
4872 ScanRange::from_parts(
4873 BlockHeight::from_u32(UNSCANNED_END)
4874 ..BlockHeight::from_u32(CHAIN_HEIGHT + 1),
4875 ScanPriority::Scanned,
4876 ),
4877 ],
4878 transactions,
4879 )
4880 }
4881
4882 fn wallet_with_scan_ranges(
4883 scan_ranges: Vec<ScanRange>,
4884 transactions: Vec<WalletTransaction>,
4885 ) -> MockWallet {
4886 let sync_state = SyncState {
4887 scan_ranges,
4888 ..Default::default()
4889 };
4890 let wallet_transactions: HashMap<TxId, WalletTransaction> = transactions
4891 .into_iter()
4892 .map(|transaction| (transaction.txid(), transaction))
4893 .collect();
4894
4895 MockWalletBuilder::new()
4896 .sync_state(sync_state)
4897 .wallet_transactions(wallet_transactions)
4898 .create_mock_wallet()
4899 }
4900
4901 fn transaction_status(wallet: &MockWallet, txid: TxId) -> ConfirmationStatus {
4902 crate::wallet::traits::SyncTransactions::get_wallet_transactions(wallet)
4903 .unwrap()
4904 .get(&txid)
4905 .unwrap()
4906 .status()
4907 }
4908
4909 #[test]
4910 fn pending_transaction_past_expiry_is_failed() {
4911 let txid = TxId::from_bytes([1; 32]);
4912 let transaction = WalletTransaction::new_for_test_with_expiry(
4913 txid,
4914 ConfirmationStatus::Mempool(BlockHeight::from_u32(61)),
4915 BlockHeight::from_u32(100),
4916 );
4917 let mut wallet = wallet_at_height(100, vec![transaction]);
4918
4919 expire_transactions(&mut wallet).unwrap();
4920
4921 assert!(matches!(
4922 transaction_status(&wallet, txid),
4923 ConfirmationStatus::Failed(_)
4924 ));
4925 }
4926
4927 #[test]
4928 fn pending_transaction_before_expiry_is_untouched() {
4929 let txid = TxId::from_bytes([1; 32]);
4930 let transaction = WalletTransaction::new_for_test_with_expiry(
4931 txid,
4932 ConfirmationStatus::Mempool(BlockHeight::from_u32(61)),
4933 BlockHeight::from_u32(101),
4934 );
4935 let mut wallet = wallet_at_height(100, vec![transaction]);
4936
4937 expire_transactions(&mut wallet).unwrap();
4938
4939 assert!(matches!(
4940 transaction_status(&wallet, txid),
4941 ConfirmationStatus::Mempool(_)
4942 ));
4943 }
4944
4945 #[test]
4946 fn zero_expiry_transaction_never_expires() {
4947 let txid = TxId::from_bytes([1; 32]);
4949 let transaction = WalletTransaction::new_for_test_with_expiry(
4950 txid,
4951 ConfirmationStatus::Mempool(BlockHeight::from_u32(61)),
4952 BlockHeight::from_u32(0),
4953 );
4954 let mut wallet = wallet_at_height(1_000_000, vec![transaction]);
4955
4956 expire_transactions(&mut wallet).unwrap();
4957
4958 assert!(matches!(
4959 transaction_status(&wallet, txid),
4960 ConfirmationStatus::Mempool(_)
4961 ));
4962 }
4963
4964 #[test]
4965 fn pending_transaction_with_expiry_above_an_unscanned_range_is_untouched() {
4966 let txid = TxId::from_bytes([1; 32]);
4967 let transaction = WalletTransaction::new_for_test_with_expiry(
4968 txid,
4969 ConfirmationStatus::Mempool(BlockHeight::from_u32(UNSCANNED_START - 1)),
4970 BlockHeight::from_u32(UNSCANNED_END),
4971 );
4972 let mut wallet = wallet_with_unscanned_range(vec![transaction]);
4973
4974 expire_transactions(&mut wallet).unwrap();
4975
4976 assert!(matches!(
4977 transaction_status(&wallet, txid),
4978 ConfirmationStatus::Mempool(_)
4979 ));
4980 }
4981
4982 #[test]
4983 fn pending_transaction_with_expiry_below_an_unscanned_range_is_failed() {
4984 let txid = TxId::from_bytes([1; 32]);
4985 let transaction = WalletTransaction::new_for_test_with_expiry(
4986 txid,
4987 ConfirmationStatus::Mempool(BlockHeight::from_u32(1)),
4988 BlockHeight::from_u32(UNSCANNED_START - 1),
4989 );
4990 let mut wallet = wallet_with_unscanned_range(vec![transaction]);
4991
4992 expire_transactions(&mut wallet).unwrap();
4993
4994 assert!(matches!(
4995 transaction_status(&wallet, txid),
4996 ConfirmationStatus::Failed(_)
4997 ));
4998 }
4999
5000 #[test]
5001 fn confirmed_transaction_is_untouched() {
5002 let txid = TxId::from_bytes([1; 32]);
5003 let transaction = WalletTransaction::new_for_test_with_expiry(
5004 txid,
5005 ConfirmationStatus::Confirmed(BlockHeight::from_u32(61)),
5006 BlockHeight::from_u32(100),
5007 );
5008 let mut wallet = wallet_at_height(100, vec![transaction]);
5009
5010 expire_transactions(&mut wallet).unwrap();
5011
5012 assert!(matches!(
5013 transaction_status(&wallet, txid),
5014 ConfirmationStatus::Confirmed(_)
5015 ));
5016 }
5017 }
5018
5019 mod stale_scan_results {
5021 use std::collections::{BTreeMap, BTreeSet, HashMap};
5022
5023 use tokio::sync::mpsc;
5024 use zcash_primitives::block::BlockHash;
5025 use zcash_protocol::consensus::BlockHeight;
5026 use zcash_transparent::keys::NonHardenedChildIndex;
5027
5028 use crate::{
5029 config::PerformanceLevel,
5030 error::{ContinuityError, ScanError},
5031 keys::transparent::{TransparentAddressId, TransparentScope},
5032 mocks::{MockWallet, MockWalletBuilder},
5033 scan::{
5034 ScanResults,
5035 task::{ScanLoad, TaskId},
5036 },
5037 sync::{ProcessedScanResults, ScanPriority, ScanRange, process_scan_results},
5038 wallet::{NullifierMap, SyncState, traits::SyncWallet as _},
5039 };
5040
5041 use super::NETWORK;
5042 const BIRTHDAY: u32 = 1;
5043 const CHAIN_HEIGHT: u32 = 40;
5045
5046 fn wallet_with_scan_ranges(scan_ranges: Vec<ScanRange>) -> MockWallet {
5047 MockWalletBuilder::new()
5048 .sync_state(SyncState {
5049 scan_ranges,
5050 ..Default::default()
5051 })
5052 .create_mock_wallet()
5053 }
5054
5055 async fn process(
5056 wallet: &mut MockWallet,
5057 scan_range: ScanRange,
5058 scan_results: Result<ScanResults, ScanError>,
5059 ) -> ProcessedScanResults {
5060 let (fetch_request_sender, _) = mpsc::unbounded_channel();
5061 let task_id = TaskId::first();
5062 let in_flight_tasks = BTreeMap::from([(task_id, scan_range.clone())]);
5063
5064 process_scan_results(
5065 &NETWORK,
5066 wallet,
5067 fetch_request_sender,
5068 &HashMap::new(),
5069 ScanLoad {
5070 task_id,
5071 scan_range,
5072 },
5073 &in_flight_tasks,
5074 scan_results,
5075 None,
5076 PerformanceLevel::High,
5077 &mut false,
5078 )
5079 .await
5080 .expect("stale scan results are discarded")
5081 }
5082
5083 #[tokio::test]
5087 async fn truncated_scan_range_is_reset_and_its_scan_results_discarded() {
5088 let mut wallet = wallet_with_scan_ranges(vec![ScanRange::from_parts(
5089 BlockHeight::from_u32(BIRTHDAY)..BlockHeight::from_u32(CHAIN_HEIGHT),
5090 ScanPriority::Scanning,
5091 )]);
5092 let gap_address = (
5093 "gap address".to_string(),
5094 TransparentAddressId::new(
5095 zip32::AccountId::ZERO,
5096 TransparentScope::External,
5097 NonHardenedChildIndex::ZERO,
5098 ),
5099 );
5100
5101 let processed = process(
5102 &mut wallet,
5103 ScanRange::from_parts(
5104 BlockHeight::from_u32(BIRTHDAY)..BlockHeight::from_u32(CHAIN_HEIGHT + 1),
5105 ScanPriority::ChainTip,
5106 ),
5107 Ok(ScanResults {
5108 nullifiers: NullifierMap::new(),
5109 outpoints: BTreeMap::new(),
5110 scanned_blocks: BTreeMap::new(),
5111 dropped_scan_targets: BTreeSet::new(),
5112 wallet_transactions: HashMap::new(),
5113 sapling_located_trees: Vec::new(),
5114 orchard_located_trees: Vec::new(),
5115 ironwood_located_trees: Vec::new(),
5116 new_transparent_inuse_addresses: HashMap::from([gap_address.clone()]),
5117 new_transparent_gap_addresses: HashMap::from([gap_address]),
5118 }),
5119 )
5120 .await;
5121
5122 assert!(processed.new_transparent_inuse_addresses.is_empty());
5123 assert!(processed.new_transparent_gap_addresses.is_empty());
5124 assert_eq!(
5125 wallet.get_sync_state().unwrap().scan_ranges(),
5126 [ScanRange::from_parts(
5127 BlockHeight::from_u32(BIRTHDAY)..BlockHeight::from_u32(CHAIN_HEIGHT),
5128 ScanPriority::ChainTip,
5129 )]
5130 );
5131 }
5132
5133 #[tokio::test]
5137 async fn error_of_a_removed_scan_range_is_discarded() {
5138 let scan_ranges = vec![ScanRange::from_parts(
5139 BlockHeight::from_u32(BIRTHDAY)..BlockHeight::from_u32(CHAIN_HEIGHT),
5140 ScanPriority::Scanned,
5141 )];
5142 let mut wallet = wallet_with_scan_ranges(scan_ranges.clone());
5143
5144 process(
5145 &mut wallet,
5146 ScanRange::from_parts(
5147 BlockHeight::from_u32(CHAIN_HEIGHT)..BlockHeight::from_u32(CHAIN_HEIGHT + 1),
5148 ScanPriority::Verify,
5149 ),
5150 Err(ScanError::ContinuityError(
5151 ContinuityError::HashDiscontinuity {
5152 height: BlockHeight::from_u32(CHAIN_HEIGHT),
5153 prev_hash: BlockHash([1; 32]),
5154 previous_block_hash: BlockHash([2; 32]),
5155 },
5156 )),
5157 )
5158 .await;
5159
5160 assert_eq!(wallet.get_sync_state().unwrap().scan_ranges(), scan_ranges);
5161 }
5162 }
5163
5164 mod shard_tree_checkpoints {
5169 use std::sync::Arc;
5170 use std::sync::atomic::{self, AtomicUsize};
5171
5172 use incrementalmerkletree::frontier::CommitmentTree;
5173 use incrementalmerkletree::{Hashable as _, Marking, Position, Retention};
5174 use orchard::tree::MerkleHashOrchard;
5175 use shardtree::store::{self, ShardStore as _};
5176 use tokio::sync::mpsc;
5177 use zcash_primitives::merkle_tree::write_commitment_tree;
5178 use zcash_protocol::consensus::BlockHeight;
5179 use zingo_netutils::lightwallet_protocol::TreeState;
5180
5181 use crate::{
5182 client::FetchRequest,
5183 error::SyncError,
5184 mocks::{MockWallet, MockWalletBuilder, MockWalletError},
5185 shardtree_ext::ShardTreeExt,
5186 sync::{ScanPriority, ScanRange},
5187 wallet::{ShardTrees, traits::SyncShardTrees},
5188 };
5189
5190 use super::NETWORK;
5191 const HIGHEST_SCANNED: u32 = 10;
5192 const FIRST_SCANNED_BLOCK: u32 = HIGHEST_SCANNED + 1;
5193 const SECOND_SCANNED_BLOCK: u32 = FIRST_SCANNED_BLOCK + 1;
5194 const SHIELDED_POOLS: usize = 3;
5195 const ORCHARD_TREE_SIZE: u64 = 3;
5197
5198 async fn update_shard_trees(
5201 wallet: &mut MockWallet,
5202 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
5203 ) -> Result<(), SyncError<MockWalletError>> {
5204 wallet
5205 .update_shard_trees(
5206 &NETWORK,
5207 fetch_request_sender,
5208 &ScanRange::from_parts(
5209 BlockHeight::from_u32(FIRST_SCANNED_BLOCK)
5210 ..BlockHeight::from_u32(SECOND_SCANNED_BLOCK + 1),
5211 ScanPriority::ChainTip,
5212 ),
5213 BlockHeight::from_u32(HIGHEST_SCANNED),
5214 None,
5215 Vec::new(),
5216 Vec::new(),
5217 Vec::new(),
5218 )
5219 .await
5220 }
5221
5222 fn wallet(shard_trees: ShardTrees) -> MockWallet {
5223 MockWalletBuilder::new()
5224 .shard_trees(shard_trees)
5225 .create_mock_wallet()
5226 }
5227
5228 fn failing_fetcher() -> mpsc::UnboundedSender<FetchRequest> {
5230 let (fetch_request_sender, fetch_request_receiver) = mpsc::unbounded_channel();
5231 drop(fetch_request_receiver);
5232
5233 fetch_request_sender
5234 }
5235
5236 fn spawn_counting_fetcher() -> (mpsc::UnboundedSender<FetchRequest>, Arc<AtomicUsize>) {
5239 const EMPTY_TREE: &str = "000000";
5240 let mut orchard_tree = CommitmentTree::<
5241 MerkleHashOrchard,
5242 { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
5243 >::empty();
5244 for _ in 0..ORCHARD_TREE_SIZE {
5245 orchard_tree
5246 .append(MerkleHashOrchard::empty_leaf())
5247 .unwrap();
5248 }
5249 let mut orchard_tree_bytes = Vec::new();
5250 write_commitment_tree(&orchard_tree, &mut orchard_tree_bytes).unwrap();
5251 let orchard_tree = hex::encode(orchard_tree_bytes);
5252
5253 let request_count = Arc::new(AtomicUsize::new(0));
5254 let counted_requests = request_count.clone();
5255 let (fetch_request_sender, mut fetch_request_receiver) = mpsc::unbounded_channel();
5256 tokio::spawn(async move {
5257 while let Some(fetch_request) = fetch_request_receiver.recv().await {
5258 match fetch_request {
5259 FetchRequest::TreeState(reply_sender, block_height) => {
5260 counted_requests.fetch_add(1, atomic::Ordering::Release);
5261 let _ignore_error = reply_sender.send(Ok(TreeState {
5262 height: u64::from(block_height),
5263 hash: "00".repeat(32),
5264 sapling_tree: EMPTY_TREE.to_string(),
5265 orchard_tree: orchard_tree.clone(),
5266 ironwood_tree: EMPTY_TREE.to_string(),
5267 ..Default::default()
5268 }));
5269 }
5270 _ => panic!("unexpected fetch request"),
5271 }
5272 }
5273 });
5274
5275 (fetch_request_sender, request_count)
5276 }
5277
5278 fn checkpoint_tree_states(
5280 wallet: &mut MockWallet,
5281 height: u32,
5282 ) -> [Option<store::TreeState>; SHIELDED_POOLS] {
5283 let height = BlockHeight::from_u32(height);
5284 let shard_trees = wallet.get_shard_trees_mut().unwrap();
5285 [
5286 shard_trees.sapling.store().get_checkpoint(&height).unwrap(),
5287 shard_trees.orchard.store().get_checkpoint(&height).unwrap(),
5288 shard_trees
5289 .ironwood
5290 .store()
5291 .get_checkpoint(&height)
5292 .unwrap(),
5293 ]
5294 .map(|checkpoint| checkpoint.map(|checkpoint| checkpoint.tree_state()))
5295 }
5296
5297 #[tokio::test]
5300 async fn checkpoints_copy_the_block_below() {
5301 let mut shard_trees = ShardTrees::new();
5302 let highest_scanned_height = BlockHeight::from_u32(HIGHEST_SCANNED);
5303 shard_trees
5304 .sapling
5305 .append_checkpoint(highest_scanned_height)
5306 .unwrap();
5307 shard_trees
5308 .orchard
5309 .append(
5310 MerkleHashOrchard::empty_leaf(),
5311 Retention::Checkpoint {
5312 id: highest_scanned_height,
5313 marking: Marking::None,
5314 },
5315 )
5316 .unwrap();
5317 shard_trees
5318 .ironwood
5319 .append_checkpoint(highest_scanned_height)
5320 .unwrap();
5321 let mut wallet = wallet(shard_trees);
5322
5323 update_shard_trees(&mut wallet, failing_fetcher())
5324 .await
5325 .unwrap();
5326
5327 let highest_scanned_tree_states = [
5328 Some(store::TreeState::Empty),
5329 Some(store::TreeState::AtPosition(Position::from(0))),
5330 Some(store::TreeState::Empty),
5331 ];
5332 assert_eq!(
5333 checkpoint_tree_states(&mut wallet, FIRST_SCANNED_BLOCK),
5334 highest_scanned_tree_states
5335 );
5336 assert_eq!(
5337 checkpoint_tree_states(&mut wallet, SECOND_SCANNED_BLOCK),
5338 highest_scanned_tree_states
5339 );
5340 }
5341
5342 #[tokio::test]
5346 async fn missing_checkpoint_below_is_fetched_once_for_each_pool() {
5347 let mut wallet = wallet(ShardTrees::new());
5348 let (fetch_request_sender, request_count) = spawn_counting_fetcher();
5349
5350 update_shard_trees(&mut wallet, fetch_request_sender)
5351 .await
5352 .unwrap();
5353
5354 let fetched_tree_states = [
5355 Some(store::TreeState::Empty),
5356 Some(store::TreeState::AtPosition(Position::from(
5357 ORCHARD_TREE_SIZE - 1,
5358 ))),
5359 Some(store::TreeState::Empty),
5360 ];
5361 assert_eq!(
5362 checkpoint_tree_states(&mut wallet, FIRST_SCANNED_BLOCK),
5363 fetched_tree_states
5364 );
5365 assert_eq!(
5366 checkpoint_tree_states(&mut wallet, SECOND_SCANNED_BLOCK),
5367 fetched_tree_states
5368 );
5369 assert_eq!(
5370 request_count.load(atomic::Ordering::Acquire),
5371 SHIELDED_POOLS
5372 );
5373 }
5374
5375 #[tokio::test]
5379 async fn failed_request_leaves_the_shard_trees_as_they_were() {
5380 let mut shard_trees = ShardTrees::new();
5381 shard_trees
5382 .sapling
5383 .append_checkpoint(BlockHeight::from_u32(HIGHEST_SCANNED))
5384 .unwrap();
5385 let mut wallet = wallet(shard_trees);
5386
5387 let result = update_shard_trees(&mut wallet, failing_fetcher()).await;
5388
5389 assert!(result.is_err());
5390 assert_eq!(
5391 checkpoint_tree_states(&mut wallet, FIRST_SCANNED_BLOCK),
5392 [None; SHIELDED_POOLS]
5393 );
5394 }
5395 }
5396
5397 mod scan_results_wallet_update {
5399 use std::collections::{BTreeMap, BTreeSet, HashMap};
5400
5401 use incrementalmerkletree::{Hashable as _, Marking, Position, Retention};
5402 use orchard::tree::MerkleHashOrchard;
5403 use shardtree::store::ShardStore as _;
5404 use tokio::sync::mpsc;
5405 use zcash_primitives::{block::BlockHash, transaction::TxId};
5406 use zcash_protocol::consensus::BlockHeight;
5407 use zingo_netutils::lightwallet_protocol::{RawTransaction, TreeState};
5408 use zingo_status::confirmation_status::ConfirmationStatus;
5409
5410 use super::spend_reset_lifecycle::{
5411 FUNDING_HEIGHT, FUNDING_TXID, NOTE_NULLIFIER, SPEND_HEIGHT, funding_transaction,
5412 spending_key,
5413 };
5414 use crate::{
5415 client::FetchRequest,
5416 config::PerformanceLevel,
5417 error::SyncError,
5418 mocks::{MockWallet, MockWalletBuilder, MockWalletError},
5419 scan::{
5420 ScanResults,
5421 task::{ScanLoad, TaskId},
5422 },
5423 sync::{ProcessedScanResults, ScanPriority, ScanRange, process_scan_results},
5424 wallet::{
5425 NullifierMap, OutputId, ScanTarget, SyncState, TreeBounds, WalletBlock,
5426 WalletTransaction,
5427 traits::{
5428 SyncBlocks, SyncNullifiers, SyncOutPoints, SyncShardTrees, SyncTransactions,
5429 SyncWallet,
5430 },
5431 },
5432 witness,
5433 };
5434
5435 use super::NETWORK;
5436 const BIRTHDAY: u32 = 1;
5437 const SCANNED_TXID: TxId = TxId::from_bytes([3; 32]);
5439
5440 fn spending_transaction() -> WalletTransaction {
5442 let status = ConfirmationStatus::Confirmed(SPEND_HEIGHT);
5443 let txid = WalletTransaction::new_for_test(TxId::from_bytes([0; 32]), status)
5444 .transaction()
5445 .txid();
5446 WalletTransaction::new_for_test(txid, status)
5447 }
5448
5449 fn spawn_fetcher(
5452 spending_transaction: Option<&WalletTransaction>,
5453 ) -> mpsc::UnboundedSender<FetchRequest> {
5454 const EMPTY_TREE: &str = "000000";
5455 let spending_transaction_data = spending_transaction.map(|spending_transaction| {
5456 let mut data = Vec::new();
5457 spending_transaction.transaction().write(&mut data).unwrap();
5458 data
5459 });
5460 let (fetch_request_sender, mut fetch_request_receiver) = mpsc::unbounded_channel();
5461 tokio::spawn(async move {
5462 while let Some(fetch_request) = fetch_request_receiver.recv().await {
5463 match fetch_request {
5464 FetchRequest::TreeState(reply_sender, block_height) => {
5465 let _ignore_error = reply_sender.send(Ok(TreeState {
5466 height: u64::from(block_height),
5467 hash: "00".repeat(32),
5468 sapling_tree: EMPTY_TREE.to_string(),
5469 orchard_tree: EMPTY_TREE.to_string(),
5470 ironwood_tree: EMPTY_TREE.to_string(),
5471 ..Default::default()
5472 }));
5473 }
5474 FetchRequest::Transaction(reply_sender, _txid) => {
5475 if let Some(data) = &spending_transaction_data {
5477 let _ignore_error = reply_sender.send(Ok(RawTransaction {
5478 data: data.clone(),
5479 height: u64::from(SPEND_HEIGHT),
5480 }));
5481 }
5482 }
5483 _ => panic!("unexpected fetch request"),
5484 }
5485 }
5486 });
5487
5488 fetch_request_sender
5489 }
5490
5491 fn block_at_spend_height() -> WalletBlock {
5493 WalletBlock {
5494 block_height: SPEND_HEIGHT,
5495 block_hash: BlockHash([0; 32]),
5496 prev_hash: BlockHash([0; 32]),
5497 time: 0,
5498 txids: Vec::new(),
5499 tree_bounds: TreeBounds {
5500 sapling_initial_tree_size: 0,
5501 sapling_final_tree_size: 0,
5502 orchard_initial_tree_size: 0,
5503 orchard_final_tree_size: 1,
5504 ironwood_initial_tree_size: 0,
5505 ironwood_final_tree_size: 0,
5506 },
5507 }
5508 }
5509
5510 fn scan_ranges(spend_height_priority: ScanPriority) -> Vec<ScanRange> {
5513 vec![
5514 ScanRange::from_parts(
5515 BlockHeight::from_u32(BIRTHDAY)..SPEND_HEIGHT,
5516 ScanPriority::Scanned,
5517 ),
5518 ScanRange::from_parts(SPEND_HEIGHT..SPEND_HEIGHT + 1, spend_height_priority),
5519 ]
5520 }
5521
5522 fn scanned_scan_ranges() -> Vec<ScanRange> {
5524 vec![ScanRange::from_parts(
5525 BlockHeight::from_u32(BIRTHDAY)..SPEND_HEIGHT + 1,
5526 ScanPriority::Scanned,
5527 )]
5528 }
5529
5530 fn wallet_builder(spend_height_priority: ScanPriority) -> MockWalletBuilder {
5532 MockWalletBuilder::new()
5533 .birthday(BlockHeight::from_u32(BIRTHDAY))
5534 .sync_state(SyncState::new_for_test(scan_ranges(spend_height_priority)))
5535 .wallet_transactions(HashMap::from([(FUNDING_TXID, funding_transaction(None))]))
5536 }
5537
5538 fn block_at_funding_height() -> WalletBlock {
5540 WalletBlock {
5541 block_height: FUNDING_HEIGHT,
5542 block_hash: BlockHash([0; 32]),
5543 prev_hash: BlockHash([0; 32]),
5544 time: 0,
5545 txids: Vec::new(),
5546 tree_bounds: TreeBounds {
5547 sapling_initial_tree_size: 0,
5548 sapling_final_tree_size: 0,
5549 orchard_initial_tree_size: 0,
5550 orchard_final_tree_size: 0,
5551 ironwood_initial_tree_size: 0,
5552 ironwood_final_tree_size: 0,
5553 },
5554 }
5555 }
5556
5557 fn funding_scan_ranges(funding_height_priority: ScanPriority) -> Vec<ScanRange> {
5560 vec![
5561 ScanRange::from_parts(
5562 BlockHeight::from_u32(BIRTHDAY)..FUNDING_HEIGHT,
5563 ScanPriority::Scanned,
5564 ),
5565 ScanRange::from_parts(FUNDING_HEIGHT..FUNDING_HEIGHT + 1, funding_height_priority),
5566 ScanRange::from_parts(FUNDING_HEIGHT + 1..SPEND_HEIGHT + 1, ScanPriority::Scanned),
5567 ]
5568 }
5569
5570 fn wallet_with_mapped_spend(spending_txid: TxId) -> MockWallet {
5574 MockWalletBuilder::new()
5575 .birthday(BlockHeight::from_u32(BIRTHDAY))
5576 .sync_state(SyncState::new_for_test(funding_scan_ranges(
5577 ScanPriority::Scanning,
5578 )))
5579 .nullifier_map(mapped_spend(spending_txid))
5580 .wallet_blocks(BTreeMap::from([(SPEND_HEIGHT, block_at_spend_height())]))
5581 .create_mock_wallet()
5582 }
5583
5584 fn funding_scan_results() -> ScanResults {
5587 ScanResults {
5588 nullifiers: NullifierMap::new(),
5589 outpoints: BTreeMap::new(),
5590 scanned_blocks: BTreeMap::from([(FUNDING_HEIGHT, block_at_funding_height())]),
5591 dropped_scan_targets: BTreeSet::new(),
5592 wallet_transactions: HashMap::from([(FUNDING_TXID, funding_transaction(None))]),
5593 sapling_located_trees: Vec::new(),
5594 orchard_located_trees: Vec::new(),
5595 ironwood_located_trees: Vec::new(),
5596 new_transparent_inuse_addresses: HashMap::new(),
5597 new_transparent_gap_addresses: HashMap::new(),
5598 }
5599 }
5600
5601 fn mapped_spend(spending_txid: TxId) -> NullifierMap {
5604 let mut nullifiers = NullifierMap::new();
5605 nullifiers.sapling.insert(
5606 NOTE_NULLIFIER,
5607 ScanTarget::internal(SPEND_HEIGHT, spending_txid),
5608 );
5609
5610 nullifiers
5611 }
5612
5613 fn scan_results(spending_txid: TxId) -> ScanResults {
5617 const LOCATED_TREE_SIZE: usize = 1;
5618 ScanResults {
5619 nullifiers: mapped_spend(spending_txid),
5620 outpoints: BTreeMap::from([(
5621 OutputId::new(SCANNED_TXID, 0),
5622 ScanTarget::internal(SPEND_HEIGHT, spending_txid),
5623 )]),
5624 scanned_blocks: BTreeMap::from([(SPEND_HEIGHT, block_at_spend_height())]),
5625 dropped_scan_targets: BTreeSet::new(),
5626 wallet_transactions: HashMap::from([(
5627 SCANNED_TXID,
5628 WalletTransaction::new_for_test(
5629 SCANNED_TXID,
5630 ConfirmationStatus::Confirmed(SPEND_HEIGHT),
5631 ),
5632 )]),
5633 sapling_located_trees: Vec::new(),
5634 orchard_located_trees: witness::build_located_trees(
5635 Position::from(0),
5636 vec![(
5637 MerkleHashOrchard::empty_leaf(),
5638 Retention::Checkpoint {
5639 id: SPEND_HEIGHT,
5640 marking: Marking::None,
5641 },
5642 )],
5643 LOCATED_TREE_SIZE,
5644 ),
5645 ironwood_located_trees: Vec::new(),
5646 new_transparent_inuse_addresses: HashMap::new(),
5647 new_transparent_gap_addresses: HashMap::new(),
5648 }
5649 }
5650
5651 async fn process(
5654 wallet: &mut MockWallet,
5655 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
5656 selected_priority: ScanPriority,
5657 scan_results: ScanResults,
5658 ) -> Result<ProcessedScanResults, SyncError<MockWalletError>> {
5659 process_block(
5660 wallet,
5661 fetch_request_sender,
5662 SPEND_HEIGHT,
5663 selected_priority,
5664 scan_results,
5665 )
5666 .await
5667 }
5668
5669 async fn process_block(
5672 wallet: &mut MockWallet,
5673 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
5674 block_height: BlockHeight,
5675 selected_priority: ScanPriority,
5676 scan_results: ScanResults,
5677 ) -> Result<ProcessedScanResults, SyncError<MockWalletError>> {
5678 let ufvks = HashMap::from([(
5679 zip32::AccountId::ZERO,
5680 spending_key().to_unified_full_viewing_key(),
5681 )]);
5682 let task_id = TaskId::first();
5683 let scan_range =
5684 ScanRange::from_parts(block_height..block_height + 1, selected_priority);
5685 let in_flight_tasks = BTreeMap::from([(task_id, scan_range.clone())]);
5686
5687 process_scan_results(
5688 &NETWORK,
5689 wallet,
5690 fetch_request_sender,
5691 &ufvks,
5692 ScanLoad {
5693 task_id,
5694 scan_range,
5695 },
5696 &in_flight_tasks,
5697 Ok(scan_results),
5698 None,
5699 PerformanceLevel::High,
5700 &mut false,
5701 )
5702 .await
5703 }
5704
5705 fn note_spending_txid(wallet: &MockWallet) -> Option<TxId> {
5706 wallet.get_wallet_transactions().unwrap()[&FUNDING_TXID].sapling_notes()[0]
5707 .spending_transaction
5708 }
5709
5710 fn orchard_tree_size(wallet: &mut MockWallet) -> Option<Position> {
5711 wallet
5712 .get_shard_trees_mut()
5713 .unwrap()
5714 .orchard
5715 .max_leaf_position(None)
5716 .unwrap()
5717 }
5718
5719 #[tokio::test]
5724 async fn scan_results_are_added_to_the_wallet() {
5725 let spending_transaction = spending_transaction();
5726 let spending_txid = spending_transaction.txid();
5727 let mut wallet = wallet_builder(ScanPriority::Scanning).create_mock_wallet();
5728
5729 let result = process(
5730 &mut wallet,
5731 spawn_fetcher(Some(&spending_transaction)),
5732 ScanPriority::ChainTip,
5733 scan_results(spending_txid),
5734 )
5735 .await;
5736
5737 assert!(result.is_ok());
5738 assert_eq!(note_spending_txid(&wallet), Some(spending_txid));
5739 let wallet_transactions = wallet.get_wallet_transactions().unwrap();
5740 assert!(wallet_transactions.contains_key(&SCANNED_TXID));
5741 assert_eq!(
5742 wallet_transactions[&spending_txid].status(),
5743 ConfirmationStatus::Confirmed(SPEND_HEIGHT)
5744 );
5745 assert_eq!(orchard_tree_size(&mut wallet), Some(Position::from(0)));
5746 let shard_trees = wallet.get_shard_trees_mut().unwrap();
5747 let sapling_checkpoint = shard_trees
5748 .sapling
5749 .store()
5750 .get_checkpoint(&SPEND_HEIGHT)
5751 .unwrap();
5752 let orchard_checkpoint = shard_trees
5753 .orchard
5754 .store()
5755 .get_checkpoint(&SPEND_HEIGHT)
5756 .unwrap();
5757 let ironwood_checkpoint = shard_trees
5758 .ironwood
5759 .store()
5760 .get_checkpoint(&SPEND_HEIGHT)
5761 .unwrap();
5762 assert!(
5763 sapling_checkpoint.is_some()
5764 && orchard_checkpoint.is_some()
5765 && ironwood_checkpoint.is_some(),
5766 "every shard tree must hold a checkpoint at the scanned block"
5767 );
5768 assert!(wallet.get_wallet_block(SPEND_HEIGHT).is_ok());
5769 assert_eq!(
5770 wallet.get_sync_state().unwrap().scan_ranges(),
5771 scanned_scan_ranges()
5772 );
5773 assert!(wallet.get_nullifiers().unwrap().sapling.is_empty());
5775 assert!(wallet.get_outpoints().unwrap().is_empty());
5776 }
5777
5778 #[tokio::test]
5782 async fn failed_spending_transaction_request_leaves_the_wallet_as_it_was() {
5783 let mut wallet = wallet_builder(ScanPriority::Scanning).create_mock_wallet();
5784
5785 let result = process(
5786 &mut wallet,
5787 spawn_fetcher(None),
5788 ScanPriority::ChainTip,
5789 scan_results(spending_transaction().txid()),
5790 )
5791 .await;
5792
5793 assert!(result.is_err());
5794 assert!(wallet.get_nullifiers().unwrap().sapling.is_empty());
5795 assert!(wallet.get_outpoints().unwrap().is_empty());
5796 assert_eq!(orchard_tree_size(&mut wallet), None);
5797 assert!(wallet.get_wallet_block(SPEND_HEIGHT).is_err());
5798 assert_eq!(wallet.get_wallet_transactions().unwrap().len(), 1);
5799 assert_eq!(note_spending_txid(&wallet), None);
5800 assert_eq!(
5801 wallet.get_sync_state().unwrap().scan_ranges(),
5802 scan_ranges(ScanPriority::Scanning)
5803 );
5804 }
5805
5806 #[tokio::test]
5811 async fn refetched_nullifiers_record_the_spend() {
5812 let spending_transaction = spending_transaction();
5813 let spending_txid = spending_transaction.txid();
5814 let mut wallet = wallet_builder(ScanPriority::RefetchingNullifiers)
5815 .wallet_blocks(BTreeMap::from([(SPEND_HEIGHT, block_at_spend_height())]))
5816 .create_mock_wallet();
5817 let scan_results = ScanResults {
5818 nullifiers: mapped_spend(spending_txid),
5819 outpoints: BTreeMap::new(),
5820 scanned_blocks: BTreeMap::new(),
5821 wallet_transactions: HashMap::new(),
5822 dropped_scan_targets: BTreeSet::new(),
5823 sapling_located_trees: Vec::new(),
5824 orchard_located_trees: Vec::new(),
5825 ironwood_located_trees: Vec::new(),
5826 new_transparent_inuse_addresses: HashMap::new(),
5827 new_transparent_gap_addresses: HashMap::new(),
5828 };
5829
5830 let result = process(
5831 &mut wallet,
5832 spawn_fetcher(Some(&spending_transaction)),
5833 ScanPriority::ScannedWithoutMapping,
5834 scan_results,
5835 )
5836 .await;
5837
5838 assert!(result.is_ok());
5839 assert_eq!(note_spending_txid(&wallet), Some(spending_txid));
5840 assert_eq!(
5841 wallet.get_wallet_transactions().unwrap()[&spending_txid].status(),
5842 ConfirmationStatus::Confirmed(SPEND_HEIGHT)
5843 );
5844 assert_eq!(orchard_tree_size(&mut wallet), None);
5845 assert_eq!(
5846 wallet.get_sync_state().unwrap().scan_ranges(),
5847 scanned_scan_ranges()
5848 );
5849 }
5850
5851 #[tokio::test]
5854 async fn failed_request_for_refetched_nullifiers_leaves_the_wallet_as_it_was() {
5855 let mut wallet = wallet_builder(ScanPriority::RefetchingNullifiers)
5856 .wallet_blocks(BTreeMap::from([(SPEND_HEIGHT, block_at_spend_height())]))
5857 .create_mock_wallet();
5858 let scan_results = ScanResults {
5859 nullifiers: mapped_spend(spending_transaction().txid()),
5860 outpoints: BTreeMap::new(),
5861 scanned_blocks: BTreeMap::new(),
5862 wallet_transactions: HashMap::new(),
5863 dropped_scan_targets: BTreeSet::new(),
5864 sapling_located_trees: Vec::new(),
5865 orchard_located_trees: Vec::new(),
5866 ironwood_located_trees: Vec::new(),
5867 new_transparent_inuse_addresses: HashMap::new(),
5868 new_transparent_gap_addresses: HashMap::new(),
5869 };
5870
5871 let result = process(
5872 &mut wallet,
5873 spawn_fetcher(None),
5874 ScanPriority::ScannedWithoutMapping,
5875 scan_results,
5876 )
5877 .await;
5878
5879 assert!(result.is_err());
5880 assert_eq!(wallet.get_wallet_transactions().unwrap().len(), 1);
5881 assert_eq!(note_spending_txid(&wallet), None);
5882 assert_eq!(
5883 wallet.get_sync_state().unwrap().scan_ranges(),
5884 scan_ranges(ScanPriority::RefetchingNullifiers)
5885 );
5886 }
5887
5888 #[tokio::test]
5893 async fn mapped_spend_is_recorded_when_the_funding_block_is_scanned() {
5894 let spending_transaction = spending_transaction();
5895 let spending_txid = spending_transaction.txid();
5896 let mut wallet = wallet_with_mapped_spend(spending_txid);
5897
5898 let result = process_block(
5899 &mut wallet,
5900 spawn_fetcher(Some(&spending_transaction)),
5901 FUNDING_HEIGHT,
5902 ScanPriority::Historic,
5903 funding_scan_results(),
5904 )
5905 .await;
5906
5907 assert!(result.is_ok());
5908 assert_eq!(note_spending_txid(&wallet), Some(spending_txid));
5909 assert_eq!(
5910 wallet.get_wallet_transactions().unwrap()[&spending_txid].status(),
5911 ConfirmationStatus::Confirmed(SPEND_HEIGHT)
5912 );
5913 assert!(wallet.get_wallet_block(FUNDING_HEIGHT).is_ok());
5914 assert_eq!(
5915 wallet.get_sync_state().unwrap().scan_ranges(),
5916 scanned_scan_ranges()
5917 );
5918 assert!(wallet.get_nullifiers().unwrap().sapling.is_empty());
5919 }
5920
5921 #[tokio::test]
5928 async fn failed_request_keeps_the_mapped_spend() {
5929 let spending_txid = spending_transaction().txid();
5930 let mut wallet = wallet_with_mapped_spend(spending_txid);
5931
5932 let result = process_block(
5933 &mut wallet,
5934 spawn_fetcher(None),
5935 FUNDING_HEIGHT,
5936 ScanPriority::Historic,
5937 funding_scan_results(),
5938 )
5939 .await;
5940
5941 assert!(result.is_err());
5942 assert_eq!(
5943 wallet
5944 .get_nullifiers()
5945 .unwrap()
5946 .sapling
5947 .get(&NOTE_NULLIFIER)
5948 .and_then(ScanTarget::txid),
5949 Some(spending_txid),
5950 "the spend must stay mapped until it is recorded on the note"
5951 );
5952 assert!(wallet.get_wallet_transactions().unwrap().is_empty());
5953 assert!(wallet.get_wallet_block(FUNDING_HEIGHT).is_err());
5954 assert_eq!(
5955 wallet.get_sync_state().unwrap().scan_ranges(),
5956 funding_scan_ranges(ScanPriority::Scanning)
5957 );
5958 }
5959 }
5960
5961 mod hash_discontinuity {
5963 use std::collections::{BTreeMap, HashMap};
5964
5965 use tokio::sync::mpsc;
5966 use zcash_primitives::block::BlockHash;
5967 use zcash_protocol::consensus::BlockHeight;
5968 use zingo_netutils::lightwallet_protocol::TreeState;
5969
5970 use crate::{
5971 client::FetchRequest,
5972 config::PerformanceLevel,
5973 error::{ContinuityError, ScanError, SyncError},
5974 mocks::{MockWallet, MockWalletBuilder, MockWalletError},
5975 scan::task::{ScanLoad, TaskId},
5976 sync::{ProcessedScanResults, ScanPriority, ScanRange, process_scan_results},
5977 wallet::{SyncState, TreeBounds, WalletBlock, traits::SyncWallet as _},
5978 };
5979
5980 use super::NETWORK;
5981 const BIRTHDAY: u32 = 1;
5982 const SCAN_RANGE_START: u32 = 21;
5984 const SCAN_RANGE_END: u32 = 41;
5985 const VERIFY_BLOCK_RANGE_SIZE: u32 = crate::sync::VERIFY_BLOCK_RANGE_SIZE;
5986
5987 fn scan_range(start: u32, end: u32, priority: ScanPriority) -> ScanRange {
5988 ScanRange::from_parts(
5989 BlockHeight::from_u32(start)..BlockHeight::from_u32(end),
5990 priority,
5991 )
5992 }
5993
5994 fn block(height: u32) -> (BlockHeight, WalletBlock) {
5995 (
5996 BlockHeight::from_u32(height),
5997 WalletBlock {
5998 block_height: BlockHeight::from_u32(height),
5999 block_hash: BlockHash([0; 32]),
6000 prev_hash: BlockHash([0; 32]),
6001 time: 0,
6002 txids: Vec::new(),
6003 tree_bounds: TreeBounds {
6004 sapling_initial_tree_size: 0,
6005 sapling_final_tree_size: 0,
6006 orchard_initial_tree_size: 0,
6007 orchard_final_tree_size: 0,
6008 ironwood_initial_tree_size: 0,
6009 ironwood_final_tree_size: 0,
6010 },
6011 },
6012 )
6013 }
6014
6015 fn spawn_fetcher() -> mpsc::UnboundedSender<FetchRequest> {
6017 const EMPTY_TREE: &str = "000000";
6018 let (fetch_request_sender, mut fetch_request_receiver) = mpsc::unbounded_channel();
6019 tokio::spawn(async move {
6020 while let Some(fetch_request) = fetch_request_receiver.recv().await {
6021 match fetch_request {
6022 FetchRequest::TreeState(reply_sender, block_height) => {
6023 let _ignore_error = reply_sender.send(Ok(TreeState {
6024 height: u64::from(block_height),
6025 hash: "00".repeat(32),
6026 sapling_tree: EMPTY_TREE.to_string(),
6027 orchard_tree: EMPTY_TREE.to_string(),
6028 ironwood_tree: EMPTY_TREE.to_string(),
6029 ..Default::default()
6030 }));
6031 }
6032 _ => panic!("unexpected fetch request"),
6033 }
6034 }
6035 });
6036
6037 fetch_request_sender
6038 }
6039
6040 fn wallet(scan_ranges: Vec<ScanRange>, blocks: Vec<u32>) -> MockWallet {
6041 MockWalletBuilder::new()
6042 .birthday(BlockHeight::from_u32(BIRTHDAY))
6043 .sync_state(SyncState {
6044 scan_ranges,
6045 ..Default::default()
6046 })
6047 .wallet_blocks(blocks.into_iter().map(block).collect::<BTreeMap<_, _>>())
6048 .create_mock_wallet()
6049 }
6050
6051 async fn process_hash_discontinuity(
6053 wallet: &mut MockWallet,
6054 scan_range: ScanRange,
6055 height: u32,
6056 initial_reorg_detection_start_height: Option<BlockHeight>,
6057 ) -> Result<ProcessedScanResults, SyncError<MockWalletError>> {
6058 let task_id = TaskId::first();
6059 let in_flight_tasks = BTreeMap::from([(task_id, scan_range.clone())]);
6060
6061 process_scan_results(
6062 &NETWORK,
6063 wallet,
6064 spawn_fetcher(),
6065 &HashMap::new(),
6066 ScanLoad {
6067 task_id,
6068 scan_range,
6069 },
6070 &in_flight_tasks,
6071 Err(ScanError::ContinuityError(
6072 ContinuityError::HashDiscontinuity {
6073 height: BlockHeight::from_u32(height),
6074 prev_hash: BlockHash([1; 32]),
6075 previous_block_hash: BlockHash([2; 32]),
6076 },
6077 )),
6078 initial_reorg_detection_start_height,
6079 PerformanceLevel::High,
6080 &mut false,
6081 )
6082 .await
6083 }
6084
6085 #[tokio::test]
6088 async fn first_blocks_of_a_scan_range_are_verified_again() {
6089 let mut wallet = wallet(
6090 vec![
6091 scan_range(BIRTHDAY, SCAN_RANGE_START, ScanPriority::Scanned),
6092 scan_range(SCAN_RANGE_START, SCAN_RANGE_END, ScanPriority::Scanning),
6093 ],
6094 Vec::new(),
6095 );
6096
6097 let processed = process_hash_discontinuity(
6098 &mut wallet,
6099 scan_range(SCAN_RANGE_START, SCAN_RANGE_END, ScanPriority::ChainTip),
6100 SCAN_RANGE_START,
6101 None,
6102 )
6103 .await
6104 .unwrap();
6105
6106 assert_eq!(
6107 processed.reorg_detection_start_height,
6108 Some(BlockHeight::from_u32(SCAN_RANGE_START))
6109 );
6110 assert_eq!(
6111 wallet.get_sync_state().unwrap().scan_ranges(),
6112 [
6113 scan_range(BIRTHDAY, SCAN_RANGE_START, ScanPriority::Scanned),
6114 scan_range(
6115 SCAN_RANGE_START,
6116 SCAN_RANGE_START + VERIFY_BLOCK_RANGE_SIZE,
6117 ScanPriority::Verify
6118 ),
6119 scan_range(
6120 SCAN_RANGE_START + VERIFY_BLOCK_RANGE_SIZE,
6121 SCAN_RANGE_END,
6122 ScanPriority::ChainTip
6123 ),
6124 ]
6125 );
6126 }
6127
6128 #[tokio::test]
6131 async fn hash_discontinuity_above_the_first_block_is_an_error() {
6132 let scan_ranges = vec![
6133 scan_range(BIRTHDAY, SCAN_RANGE_START, ScanPriority::Scanned),
6134 scan_range(SCAN_RANGE_START, SCAN_RANGE_END, ScanPriority::Scanning),
6135 ];
6136 let mut wallet = wallet(scan_ranges.clone(), Vec::new());
6137
6138 let result = process_hash_discontinuity(
6139 &mut wallet,
6140 scan_range(SCAN_RANGE_START, SCAN_RANGE_END, ScanPriority::ChainTip),
6141 SCAN_RANGE_START + 1,
6142 None,
6143 )
6144 .await;
6145
6146 assert!(matches!(
6147 result,
6148 Err(SyncError::ScanError(ScanError::ContinuityError(
6149 ContinuityError::HashDiscontinuity { .. }
6150 )))
6151 ));
6152 assert_eq!(wallet.get_sync_state().unwrap().scan_ranges(), scan_ranges);
6153 }
6154
6155 #[tokio::test]
6159 async fn reorg_reopens_scanned_ranges_above_the_verification_range() {
6160 const VERIFY_END: u32 = SCAN_RANGE_START + VERIFY_BLOCK_RANGE_SIZE;
6161 const TRUNCATE_HEIGHT: u32 = SCAN_RANGE_START - VERIFY_BLOCK_RANGE_SIZE - 1;
6162
6163 let mut wallet = wallet(
6164 vec![
6165 scan_range(BIRTHDAY, SCAN_RANGE_START, ScanPriority::Scanned),
6166 scan_range(SCAN_RANGE_START, VERIFY_END, ScanPriority::Scanning),
6167 scan_range(VERIFY_END, SCAN_RANGE_END, ScanPriority::Scanned),
6168 ],
6169 vec![BIRTHDAY, TRUNCATE_HEIGHT],
6171 );
6172
6173 let processed = process_hash_discontinuity(
6174 &mut wallet,
6175 scan_range(SCAN_RANGE_START, VERIFY_END, ScanPriority::Verify),
6176 SCAN_RANGE_START,
6177 Some(BlockHeight::from_u32(SCAN_RANGE_START)),
6178 )
6179 .await
6180 .unwrap();
6181
6182 assert_eq!(processed.reorg_detection_start_height, None);
6183 assert_eq!(
6184 wallet.get_sync_state().unwrap().scan_ranges(),
6185 [
6186 scan_range(BIRTHDAY, TRUNCATE_HEIGHT + 1, ScanPriority::Scanned),
6187 scan_range(TRUNCATE_HEIGHT + 1, VERIFY_END, ScanPriority::Verify),
6188 scan_range(VERIFY_END, SCAN_RANGE_END, ScanPriority::Historic),
6189 ]
6190 );
6191 }
6192 }
6193
6194 mod subtree_root_fetch {
6195 use std::ops::Range;
6196
6197 use orchard::tree::MerkleHashOrchard;
6198 use zcash_protocol::consensus::BlockHeight;
6199
6200 use crate::mocks::orchard_tree_scanned_past_the_fetched_roots;
6201 use crate::sync::subtree_root_fetch_index;
6202 use crate::wallet::ShardTrees;
6203 use crate::witness;
6204
6205 fn shard_ranges(count: u32) -> Vec<Range<BlockHeight>> {
6206 (0..count)
6207 .map(|index| {
6208 let start = if index == 0 { 1 } else { index * 100 };
6209 BlockHeight::from_u32(start)..BlockHeight::from_u32((index + 1) * 100 + 1)
6210 })
6211 .collect()
6212 }
6213
6214 #[test]
6217 fn resumes_from_the_newest_recorded_root_when_scanning_carried_the_tree_ahead() {
6218 let shard_trees = orchard_tree_scanned_past_the_fetched_roots();
6219 assert_eq!(witness::subtree_fetch_start_index(&shard_trees.orchard), 5);
6220
6221 assert_eq!(
6222 subtree_root_fetch_index(&shard_trees.orchard, &shard_ranges(3)),
6223 2
6224 );
6225 }
6226
6227 #[test]
6230 fn resumes_from_the_tree_when_a_rollback_dropped_its_bare_roots() {
6231 let mut shard_trees = ShardTrees::new();
6232 let bare_root = zingo_netutils::lightwallet_protocol::SubtreeRoot {
6233 root_hash: <MerkleHashOrchard as incrementalmerkletree::Hashable>::empty_root(
6234 incrementalmerkletree::Level::from(witness::SHARD_HEIGHT),
6235 )
6236 .to_bytes()
6237 .to_vec(),
6238 ..Default::default()
6239 };
6240 witness::add_subtree_roots(0, vec![bare_root; 2], &mut shard_trees.orchard).unwrap();
6241
6242 assert_eq!(
6243 subtree_root_fetch_index(&shard_trees.orchard, &shard_ranges(3)),
6244 1
6245 );
6246 }
6247
6248 #[test]
6249 fn a_fresh_or_cleared_pool_fetches_from_the_first_root() {
6250 let shard_trees = ShardTrees::new();
6251
6252 assert_eq!(subtree_root_fetch_index(&shard_trees.orchard, &[]), 0);
6253 }
6254 }
6255
6256 mod pool_rescan {
6257 use std::collections::{BTreeMap, HashMap};
6258
6259 use incrementalmerkletree::{Hashable as _, Marking, Position, Retention};
6260 use orchard::tree::MerkleHashOrchard;
6261 use tokio::sync::mpsc;
6262 use zcash_primitives::block::BlockHash;
6263 use zcash_primitives::transaction::TxId;
6264 use zcash_protocol::consensus::{self, BlockHeight, Parameters as _};
6265 use zcash_protocol::local_consensus::LocalNetwork;
6266 use zcash_protocol::{PoolType, ShieldedPool};
6267 use zingo_netutils::lightwallet_protocol::TreeState;
6268 use zingo_status::confirmation_status::ConfirmationStatus;
6269
6270 use crate::client::FetchRequest;
6271 use crate::error::SyncError;
6272 use crate::mocks::{MockWalletBuilder, subtree_root};
6273 use crate::sync::{ScanPriority, ScanRange, state, truncate_to_pool_activation_height};
6274 use crate::wallet::{
6275 ScanTarget, ScanTargetOrigin, ShardTrees, SyncState, TreeBounds, WalletBlock,
6276 WalletTransaction,
6277 traits::{SyncShardTrees, SyncTransactions, SyncWallet},
6278 };
6279
6280 const NETWORK: LocalNetwork = LocalNetwork {
6282 nu6_3: Some(BlockHeight::from_u32(100)),
6283 ..super::NETWORK
6284 };
6285
6286 fn block(height: u32) -> WalletBlock {
6287 WalletBlock {
6288 block_height: BlockHeight::from_u32(height),
6289 block_hash: BlockHash([0; 32]),
6290 prev_hash: BlockHash([0; 32]),
6291 time: 0,
6292 txids: Vec::new(),
6293 tree_bounds: TreeBounds {
6294 sapling_initial_tree_size: 0,
6295 sapling_final_tree_size: 0,
6296 orchard_initial_tree_size: 0,
6297 orchard_final_tree_size: 0,
6298 ironwood_initial_tree_size: 0,
6299 ironwood_final_tree_size: 0,
6300 },
6301 }
6302 }
6303
6304 fn spawn_fetcher() -> mpsc::UnboundedSender<FetchRequest> {
6307 const EMPTY_TREE: &str = "000000";
6308 let (fetch_request_sender, mut fetch_request_receiver) = mpsc::unbounded_channel();
6309 tokio::spawn(async move {
6310 while let Some(fetch_request) = fetch_request_receiver.recv().await {
6311 match fetch_request {
6312 FetchRequest::TreeState(reply_sender, block_height) => {
6313 let ironwood_tree = if NETWORK
6314 .is_nu_active(consensus::NetworkUpgrade::Nu6_3, block_height)
6315 {
6316 EMPTY_TREE.to_string()
6317 } else {
6318 String::new()
6319 };
6320 let _ignore_error = reply_sender.send(Ok(TreeState {
6321 height: u64::from(block_height),
6322 hash: "00".repeat(32),
6323 sapling_tree: EMPTY_TREE.to_string(),
6324 orchard_tree: EMPTY_TREE.to_string(),
6325 ironwood_tree,
6326 ..Default::default()
6327 }));
6328 }
6329 _ => panic!("unexpected fetch request"),
6330 }
6331 }
6332 });
6333
6334 fetch_request_sender
6335 }
6336
6337 #[tokio::test]
6342 async fn rescan_clears_pool_shard_ranges() {
6343 let mut sync_state = SyncState::new_for_test(vec![ScanRange::from_parts(
6344 BlockHeight::from_u32(1)..BlockHeight::from_u32(301),
6345 ScanPriority::Scanned,
6346 )]);
6347 let orchard_shard_ranges = vec![BlockHeight::from_u32(1)..BlockHeight::from_u32(51)];
6348 sync_state.orchard_shard_ranges = orchard_shard_ranges.clone();
6349 sync_state.ironwood_shard_ranges = vec![
6350 BlockHeight::from_u32(100)..BlockHeight::from_u32(251),
6351 BlockHeight::from_u32(250)..BlockHeight::from_u32(281),
6352 ];
6353 let mut shard_trees = ShardTrees::new();
6354 shard_trees
6355 .orchard
6356 .append(
6357 MerkleHashOrchard::empty_leaf(),
6358 Retention::Checkpoint {
6359 id: BlockHeight::from_u32(50),
6360 marking: Marking::Marked,
6361 },
6362 )
6363 .unwrap();
6364 let mut wallet = MockWalletBuilder::new()
6365 .birthday(BlockHeight::from_u32(1))
6366 .sync_state(sync_state)
6367 .wallet_blocks(BTreeMap::from([(BlockHeight::from_u32(99), block(99))]))
6368 .shard_trees(shard_trees)
6369 .create_mock_wallet();
6370
6371 let result = truncate_to_pool_activation_height(
6372 &NETWORK,
6373 spawn_fetcher(),
6374 &mut wallet,
6375 ShieldedPool::Ironwood,
6376 BlockHeight::from_u32(200),
6377 10,
6378 5,
6379 )
6380 .await;
6381 assert!(matches!(
6382 result,
6383 Ok(SyncError::PoolHistoryReopened {
6384 pool: PoolType::Shielded(ShieldedPool::Ironwood),
6385 ..
6386 })
6387 ));
6388
6389 assert_eq!(
6390 wallet
6391 .get_shard_trees_mut()
6392 .unwrap()
6393 .orchard
6394 .max_leaf_position(None)
6395 .unwrap(),
6396 Some(Position::from(0)),
6397 "orchard note commitments below the ironwood activation height must be kept"
6398 );
6399
6400 let sync_state = wallet.get_sync_state_mut().unwrap();
6401 assert!(sync_state.ironwood_shard_ranges.is_empty());
6402 assert_eq!(sync_state.orchard_shard_ranges, orchard_shard_ranges);
6403
6404 state::add_shard_ranges(
6406 &NETWORK,
6407 ShieldedPool::Ironwood,
6408 sync_state,
6409 0,
6410 &[subtree_root(150), subtree_root(250)],
6411 );
6412 assert_eq!(
6413 sync_state.ironwood_shard_ranges,
6414 vec![
6415 BlockHeight::from_u32(100)..BlockHeight::from_u32(151),
6416 BlockHeight::from_u32(150)..BlockHeight::from_u32(251),
6417 ]
6418 );
6419 }
6420
6421 #[tokio::test]
6425 async fn failed_frontier_request_leaves_the_wallet_as_it_was() {
6426 const TRANSACTION_HEIGHT: u32 = 200;
6427 let txid = TxId::from_bytes([1; 32]);
6428 let scan_ranges = vec![ScanRange::from_parts(
6429 BlockHeight::from_u32(1)..BlockHeight::from_u32(301),
6430 ScanPriority::Scanned,
6431 )];
6432 let mut sync_state = SyncState::new_for_test(scan_ranges.clone());
6433 let ironwood_shard_ranges =
6434 vec![BlockHeight::from_u32(100)..BlockHeight::from_u32(251)];
6435 sync_state.ironwood_shard_ranges = ironwood_shard_ranges.clone();
6436 let mut wallet = MockWalletBuilder::new()
6437 .birthday(BlockHeight::from_u32(1))
6438 .sync_state(sync_state)
6439 .wallet_transactions(HashMap::from([(
6440 txid,
6441 WalletTransaction::new_for_test(
6442 txid,
6443 ConfirmationStatus::Confirmed(BlockHeight::from_u32(TRANSACTION_HEIGHT)),
6444 ),
6445 )]))
6446 .create_mock_wallet();
6447 let (fetch_request_sender, fetch_request_receiver) = mpsc::unbounded_channel();
6448 drop(fetch_request_receiver);
6449
6450 let result = truncate_to_pool_activation_height(
6451 &NETWORK,
6452 fetch_request_sender,
6453 &mut wallet,
6454 ShieldedPool::Ironwood,
6455 BlockHeight::from_u32(TRANSACTION_HEIGHT),
6456 10,
6457 5,
6458 )
6459 .await;
6460
6461 assert!(result.is_err());
6462 assert!(
6463 wallet
6464 .get_wallet_transactions()
6465 .unwrap()
6466 .contains_key(&txid),
6467 "the transaction must be kept while its scan range is still recorded as scanned"
6468 );
6469 let sync_state = wallet.get_sync_state().unwrap();
6470 assert_eq!(sync_state.ironwood_shard_ranges, ironwood_shard_ranges);
6471 assert_eq!(sync_state.scan_ranges(), scan_ranges);
6472 }
6473
6474 #[tokio::test]
6477 async fn rescan_targets_the_wallets_transactions_as_the_sync_engines_own() {
6478 const TRANSACTION_HEIGHT: u32 = 200;
6479 let txid = TxId::from_bytes([1; 32]);
6480 let mut wallet = MockWalletBuilder::new()
6481 .birthday(BlockHeight::from_u32(1))
6482 .sync_state(SyncState::new_for_test(vec![ScanRange::from_parts(
6483 BlockHeight::from_u32(1)..BlockHeight::from_u32(301),
6484 ScanPriority::Scanned,
6485 )]))
6486 .wallet_blocks(BTreeMap::from([(BlockHeight::from_u32(99), block(99))]))
6487 .wallet_transactions(HashMap::from([(
6488 txid,
6489 WalletTransaction::new_for_test(
6490 txid,
6491 ConfirmationStatus::Confirmed(BlockHeight::from_u32(TRANSACTION_HEIGHT)),
6492 ),
6493 )]))
6494 .create_mock_wallet();
6495
6496 truncate_to_pool_activation_height(
6497 &NETWORK,
6498 spawn_fetcher(),
6499 &mut wallet,
6500 ShieldedPool::Ironwood,
6501 BlockHeight::from_u32(TRANSACTION_HEIGHT),
6502 10,
6503 5,
6504 )
6505 .await
6506 .unwrap();
6507
6508 let targets = wallet
6509 .get_sync_state()
6510 .unwrap()
6511 .scan_targets
6512 .iter()
6513 .copied()
6514 .collect::<Vec<_>>();
6515 assert_eq!(
6516 targets,
6517 [ScanTarget::internal(
6518 BlockHeight::from_u32(TRANSACTION_HEIGHT),
6519 txid
6520 )]
6521 );
6522 assert_eq!(targets[0].origin(), ScanTargetOrigin::Internal);
6523 }
6524 }
6525
6526 mod scan_range_geometry {
6528 use zcash_protocol::consensus::BlockHeight;
6529
6530 use crate::sync::{ScanPriority, ScanRange};
6531
6532 fn blocks(start: u32, end: u32) -> std::ops::Range<BlockHeight> {
6533 BlockHeight::from_u32(start)..BlockHeight::from_u32(end)
6534 }
6535
6536 #[test]
6537 fn encloses_a_block_range_within_its_bounds() {
6538 let scan_range = ScanRange::from_parts(blocks(21, 41), ScanPriority::Scanning);
6539
6540 assert!(scan_range.encloses(&blocks(21, 41)));
6541 assert!(scan_range.encloses(&blocks(21, 31)));
6542 assert!(scan_range.encloses(&blocks(31, 41)));
6543 assert!(!scan_range.encloses(&blocks(20, 31)));
6544 assert!(!scan_range.encloses(&blocks(31, 42)));
6545 assert!(!scan_range.encloses(&blocks(1, 21)));
6546 }
6547
6548 #[test]
6549 fn overlaps_a_block_range_that_shares_a_block() {
6550 let scan_range = ScanRange::from_parts(blocks(21, 41), ScanPriority::Scanning);
6551
6552 assert!(scan_range.overlaps(&blocks(31, 51)));
6553 assert!(scan_range.overlaps(&blocks(1, 22)));
6554 assert!(scan_range.overlaps(&blocks(1, 51)));
6555 assert!(!scan_range.overlaps(&blocks(41, 51)));
6556 assert!(!scan_range.overlaps(&blocks(1, 21)));
6557 }
6558 }
6559
6560 mod in_flight_priority {
6562 use crate::sync::ScanPriority;
6563
6564 #[test]
6565 fn refetching_nullifiers_for_scanned_without_mapping_and_scanning_for_every_other_selection()
6566 {
6567 assert_eq!(
6568 ScanPriority::ScannedWithoutMapping.in_flight(),
6569 ScanPriority::RefetchingNullifiers
6570 );
6571 for selected in [
6572 ScanPriority::Historic,
6573 ScanPriority::OpenAdjacent,
6574 ScanPriority::FoundNote,
6575 ScanPriority::ChainTip,
6576 ScanPriority::Verify,
6577 ] {
6578 assert_eq!(selected.in_flight(), ScanPriority::Scanning);
6579 }
6580 }
6581 }
6582
6583 mod processed_scan_results {
6585 use crate::sync::ProcessedScanResults;
6586
6587 #[test]
6588 fn default_carries_no_addresses() {
6589 let processed = ProcessedScanResults::default();
6590
6591 assert!(processed.new_transparent_inuse_addresses.is_empty());
6592 assert!(processed.new_transparent_gap_addresses.is_empty());
6593 }
6594 }
6595
6596 mod unless_shutdown {
6598 use std::sync::atomic::{AtomicU8, Ordering};
6599 use std::time::Duration;
6600
6601 use crate::sync::{SHUTDOWN_POLL_INTERVAL, unless_shutdown};
6602 use crate::wallet::SyncMode;
6603
6604 #[tokio::test]
6605 async fn a_request_that_completes_while_running_is_served() {
6606 let sync_mode = AtomicU8::new(SyncMode::Running as u8);
6607
6608 let outcome = unless_shutdown(&sync_mode, async { 7 }).await.unwrap();
6609
6610 assert_eq!(outcome, Some(7));
6611 }
6612
6613 #[tokio::test]
6614 async fn a_pending_request_is_abandoned_when_shutdown_is_requested() {
6615 let sync_mode = AtomicU8::new(SyncMode::Running as u8);
6616 let request = std::future::pending::<()>();
6617 let stop = async {
6618 tokio::time::sleep(SHUTDOWN_POLL_INTERVAL / 2).await;
6619 sync_mode.store(SyncMode::Shutdown as u8, Ordering::Release);
6620 };
6621
6622 let (outcome, ()) = tokio::time::timeout(
6623 Duration::from_secs(5),
6624 futures::future::join(unless_shutdown(&sync_mode, request), stop),
6625 )
6626 .await
6627 .expect("the request is abandoned within the poll interval");
6628
6629 assert_eq!(outcome.unwrap(), None);
6630 }
6631
6632 #[tokio::test]
6633 async fn a_request_pending_under_pause_is_kept() {
6634 let sync_mode = AtomicU8::new(SyncMode::Paused as u8);
6635 let request = async {
6636 tokio::time::sleep(SHUTDOWN_POLL_INTERVAL * 3).await;
6637 "served"
6638 };
6639
6640 let outcome = unless_shutdown(&sync_mode, request).await.unwrap();
6641
6642 assert_eq!(outcome, Some("served"));
6643 }
6644 }
6645}