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