1use std::collections::{BTreeMap, HashMap};
4use std::ops::Range;
5use std::sync::Arc;
6use std::sync::atomic::{self, AtomicBool, AtomicU8};
7use std::time::{Duration, SystemTime};
8
9use tokio::sync::{RwLock, mpsc};
10
11use incrementalmerkletree::{Marking, Retention};
12use orchard::tree::MerkleHashOrchard;
13use shardtree::store::ShardStore;
14use tonic::transport::Channel;
15use zcash_client_backend::proto::service::RawTransaction;
16use zcash_client_backend::proto::service::compact_tx_streamer_client::CompactTxStreamerClient;
17use zcash_keys::keys::UnifiedFullViewingKey;
18use zcash_primitives::transaction::{Transaction, TxId};
19use zcash_protocol::ShieldedProtocol;
20use zcash_protocol::consensus::{self, BlockHeight};
21use zip32::AccountId;
22
23use zingo_status::confirmation_status::ConfirmationStatus;
24
25use crate::client::{self, FetchRequest};
26use crate::config::{PerformanceLevel, SyncConfig};
27use crate::error::{
28 ContinuityError, MempoolError, ScanError, ServerError, SyncError, SyncModeError,
29 SyncStatusError,
30};
31use crate::keys::transparent::TransparentAddressId;
32use crate::scan::ScanResults;
33use crate::scan::task::{Scanner, ScannerState};
34use crate::scan::transactions::scan_transaction;
35use crate::sync::state::truncate_scan_ranges;
36use crate::wallet::traits::{
37 SyncBlocks, SyncNullifiers, SyncOutPoints, SyncShardTrees, SyncTransactions, SyncWallet,
38};
39use crate::wallet::{
40 KeyIdInterface, NoteInterface, NullifierMap, OutputId, OutputInterface, ScanTarget, SyncMode,
41 SyncState, WalletBlock, WalletTransaction,
42};
43use crate::witness::LocatedTreeData;
44
45#[cfg(not(feature = "darkside_test"))]
46use crate::witness;
47
48#[cfg(not(feature = "darkside_test"))]
49pub(crate) mod transparent;
50
51pub(crate) mod spend;
52pub(crate) mod state;
53
54const UNCONFIRMED_SPEND_INVALIDATION_THRESHOLD: u32 = 3;
55pub(crate) const MAX_REORG_ALLOWANCE: u32 = 100;
56const VERIFY_BLOCK_RANGE_SIZE: u32 = 10;
57
58#[derive(Debug, Clone)]
63#[allow(missing_docs)]
64pub struct SyncStatus {
65 pub scan_ranges: Vec<ScanRange>,
66 pub sync_start_height: BlockHeight,
67 pub session_blocks_scanned: u32,
68 pub total_blocks_scanned: u32,
69 pub percentage_session_blocks_scanned: f32,
70 pub percentage_total_blocks_scanned: f32,
71 pub session_sapling_outputs_scanned: u32,
72 pub total_sapling_outputs_scanned: u32,
73 pub session_orchard_outputs_scanned: u32,
74 pub total_orchard_outputs_scanned: u32,
75 pub percentage_session_outputs_scanned: f32,
76 pub percentage_total_outputs_scanned: f32,
77}
78
79impl std::fmt::Display for SyncStatus {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(
83 f,
84 "percentage complete: {}",
85 self.percentage_total_outputs_scanned
86 )
87 }
88}
89
90impl From<SyncStatus> for json::JsonValue {
91 fn from(value: SyncStatus) -> Self {
92 let scan_ranges: Vec<json::JsonValue> = value
93 .scan_ranges
94 .iter()
95 .map(|range| {
96 json::object! {
97 "priority" => format!("{:?}", range.priority()),
98 "start_block" => range.block_range().start.to_string(),
99 "end_block" => (range.block_range().end - 1).to_string(),
100 }
101 })
102 .collect();
103
104 json::object! {
105 "scan_ranges" => scan_ranges,
106 "sync_start_height" => u32::from(value.sync_start_height),
107 "session_blocks_scanned" => value.session_blocks_scanned,
108 "total_blocks_scanned" => value.total_blocks_scanned,
109 "percentage_session_blocks_scanned" => value.percentage_session_blocks_scanned,
110 "percentage_total_blocks_scanned" => value.percentage_total_blocks_scanned,
111 "session_sapling_outputs_scanned" => value.session_sapling_outputs_scanned,
112 "total_sapling_outputs_scanned" => value.total_sapling_outputs_scanned,
113 "session_orchard_outputs_scanned" => value.session_orchard_outputs_scanned,
114 "total_orchard_outputs_scanned" => value.total_orchard_outputs_scanned,
115 "percentage_session_outputs_scanned" => value.percentage_session_outputs_scanned,
116 "percentage_total_outputs_scanned" => value.percentage_total_outputs_scanned,
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
123#[allow(missing_docs)]
124pub struct SyncResult {
125 pub sync_start_height: BlockHeight,
126 pub sync_end_height: BlockHeight,
127 pub blocks_scanned: u32,
128 pub sapling_outputs_scanned: u32,
129 pub orchard_outputs_scanned: u32,
130 pub percentage_total_outputs_scanned: f32,
131}
132
133impl std::fmt::Display for SyncResult {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 write!(
136 f,
137 "Sync completed succesfully:
138{{
139 sync start height: {}
140 sync end height: {}
141 blocks scanned: {}
142 sapling outputs scanned: {}
143 orchard outputs scanned: {}
144 percentage total outputs scanned: {}
145}}",
146 self.sync_start_height,
147 self.sync_end_height,
148 self.blocks_scanned,
149 self.sapling_outputs_scanned,
150 self.orchard_outputs_scanned,
151 self.percentage_total_outputs_scanned,
152 )
153 }
154}
155
156impl From<SyncResult> for json::JsonValue {
157 fn from(value: SyncResult) -> Self {
158 json::object! {
159 "sync_start_height" => u32::from(value.sync_start_height),
160 "sync_end_height" => u32::from(value.sync_end_height),
161 "blocks_scanned" => value.blocks_scanned,
162 "sapling_outputs_scanned" => value.sapling_outputs_scanned,
163 "orchard_outputs_scanned" => value.orchard_outputs_scanned,
164 "percentage_total_outputs_scanned" => value.percentage_total_outputs_scanned,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
171pub enum ScanPriority {
172 RefetchingNullifiers,
174 Scanning,
176 Scanned,
178 ScannedWithoutMapping,
183 Historic,
185 OpenAdjacent,
187 FoundNote,
189 ChainTip,
191 Verify,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct ScanRange {
199 block_range: Range<BlockHeight>,
200 priority: ScanPriority,
201}
202
203impl std::fmt::Display for ScanRange {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 write!(
206 f,
207 "{:?}({}..{})",
208 self.priority, self.block_range.start, self.block_range.end,
209 )
210 }
211}
212
213impl ScanRange {
214 #[must_use]
216 pub fn from_parts(block_range: Range<BlockHeight>, priority: ScanPriority) -> Self {
217 assert!(
218 block_range.end >= block_range.start,
219 "{block_range:?} is invalid for ScanRange({priority:?})",
220 );
221 ScanRange {
222 block_range,
223 priority,
224 }
225 }
226
227 #[must_use]
229 pub fn block_range(&self) -> &Range<BlockHeight> {
230 &self.block_range
231 }
232
233 #[must_use]
235 pub fn priority(&self) -> ScanPriority {
236 self.priority
237 }
238
239 #[must_use]
241 pub fn is_empty(&self) -> bool {
242 self.block_range.is_empty()
243 }
244
245 #[must_use]
247 pub fn len(&self) -> usize {
248 usize::try_from(u32::from(self.block_range.end) - u32::from(self.block_range.start))
249 .expect("due to number of max blocks should always be valid usize")
250 }
251
252 #[must_use]
256 pub fn truncate_start(&self, block_height: BlockHeight) -> Option<Self> {
257 if block_height >= self.block_range.end || self.is_empty() {
258 None
259 } else {
260 Some(ScanRange {
261 block_range: self.block_range.start.max(block_height)..self.block_range.end,
262 priority: self.priority,
263 })
264 }
265 }
266
267 #[must_use]
271 pub fn truncate_end(&self, block_height: BlockHeight) -> Option<Self> {
272 if block_height <= self.block_range.start || self.is_empty() {
273 None
274 } else {
275 Some(ScanRange {
276 block_range: self.block_range.start..self.block_range.end.min(block_height),
277 priority: self.priority,
278 })
279 }
280 }
281
282 #[must_use]
286 pub fn split_at(&self, p: BlockHeight) -> Option<(Self, Self)> {
287 (p > self.block_range.start && p < self.block_range.end).then_some((
288 ScanRange {
289 block_range: self.block_range.start..p,
290 priority: self.priority,
291 },
292 ScanRange {
293 block_range: p..self.block_range.end,
294 priority: self.priority,
295 },
296 ))
297 }
298}
299
300pub async fn sync<P, W>(
311 client: CompactTxStreamerClient<Channel>,
312 consensus_parameters: &P,
313 wallet: Arc<RwLock<W>>,
314 sync_mode: Arc<AtomicU8>,
315 config: SyncConfig,
316) -> Result<SyncResult, SyncError<W::Error>>
317where
318 P: consensus::Parameters + Sync + Send + 'static,
319 W: SyncWallet
320 + SyncBlocks
321 + SyncTransactions
322 + SyncNullifiers
323 + SyncOutPoints
324 + SyncShardTrees
325 + Send,
326{
327 let mut sync_mode_enum = SyncMode::from_atomic_u8(sync_mode.clone())?;
328 if sync_mode_enum == SyncMode::NotRunning {
329 sync_mode_enum = SyncMode::Running;
330 sync_mode.store(sync_mode_enum as u8, atomic::Ordering::Release);
331 } else {
332 return Err(SyncModeError::SyncAlreadyRunning.into());
333 }
334
335 tracing::info!("Starting sync...");
336
337 let (fetch_request_sender, fetch_request_receiver) = mpsc::unbounded_channel();
339 let client_clone = client.clone();
340 let fetcher_handle =
341 tokio::spawn(
342 async move { client::fetch::fetch(fetch_request_receiver, client_clone).await },
343 );
344
345 let (mempool_transaction_sender, mut mempool_transaction_receiver) = mpsc::channel(100);
347 let shutdown_mempool = Arc::new(AtomicBool::new(false));
348 let shutdown_mempool_clone = shutdown_mempool.clone();
349 let unprocessed_mempool_transactions_count = Arc::new(AtomicU8::new(0));
350 let unprocessed_mempool_transactions_count_clone =
351 unprocessed_mempool_transactions_count.clone();
352 let mempool_handle = tokio::spawn(async move {
353 mempool_monitor(
354 client,
355 mempool_transaction_sender,
356 unprocessed_mempool_transactions_count_clone,
357 shutdown_mempool_clone,
358 )
359 .await
360 });
361
362 let mut wallet_guard = wallet.write().await;
364
365 let chain_height = client::get_chain_height(fetch_request_sender.clone()).await?;
366 if chain_height == 0.into() {
367 return Err(SyncError::ServerError(ServerError::GenesisBlockOnly));
368 }
369 let last_known_chain_height =
370 checked_wallet_height(&mut *wallet_guard, chain_height, consensus_parameters)?;
371
372 let ufvks = wallet_guard
373 .get_unified_full_viewing_keys()
374 .map_err(SyncError::WalletError)?;
375
376 #[cfg(not(feature = "darkside_test"))]
377 transparent::update_addresses_and_scan_targets(
378 consensus_parameters,
379 &mut *wallet_guard,
380 fetch_request_sender.clone(),
381 &ufvks,
382 last_known_chain_height,
383 chain_height,
384 config.transparent_address_discovery,
385 )
386 .await?;
387
388 #[cfg(not(feature = "darkside_test"))]
389 update_subtree_roots(
390 consensus_parameters,
391 fetch_request_sender.clone(),
392 &mut *wallet_guard,
393 )
394 .await?;
395
396 add_initial_frontier(
397 consensus_parameters,
398 fetch_request_sender.clone(),
399 &mut *wallet_guard,
400 )
401 .await?;
402
403 let initial_reorg_detection_start_height = state::update_scan_ranges(
404 consensus_parameters,
405 last_known_chain_height,
406 chain_height,
407 wallet_guard
408 .get_sync_state_mut()
409 .map_err(SyncError::WalletError)?,
410 );
411
412 state::set_initial_state(
413 consensus_parameters,
414 fetch_request_sender.clone(),
415 &mut *wallet_guard,
416 chain_height,
417 )
418 .await?;
419
420 expire_transactions(&mut *wallet_guard)?;
421
422 drop(wallet_guard);
423
424 let (scan_results_sender, mut scan_results_receiver) = mpsc::unbounded_channel();
426 let mut scanner = Scanner::new(
427 consensus_parameters.clone(),
428 scan_results_sender,
429 fetch_request_sender.clone(),
430 ufvks.clone(),
431 );
432 scanner.launch(config.performance_level);
433
434 let mut nullifier_map_limit_exceeded = false;
437 let mut interval = tokio::time::interval(Duration::from_millis(50));
438 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
439 loop {
440 tokio::select! {
441 Some((scan_range, scan_results)) = scan_results_receiver.recv() => {
442 let mut wallet_guard = wallet.write().await;
443 process_scan_results(
444 consensus_parameters,
445 &mut *wallet_guard,
446 fetch_request_sender.clone(),
447 &ufvks,
448 scan_range,
449 scan_results,
450 initial_reorg_detection_start_height,
451 config.performance_level,
452 &mut nullifier_map_limit_exceeded,
453 )
454 .await?;
455 wallet_guard.set_save_flag().map_err(SyncError::WalletError)?;
456 drop(wallet_guard);
457 }
458
459 Some(raw_transaction) = mempool_transaction_receiver.recv() => {
460 let mut wallet_guard = wallet.write().await;
461 process_mempool_transaction(
462 consensus_parameters,
463 &ufvks,
464 &mut *wallet_guard,
465 raw_transaction,
466 )
467 .await?;
468 unprocessed_mempool_transactions_count.fetch_sub(1, atomic::Ordering::Release);
469 drop(wallet_guard);
470 }
471
472 _update_scanner = interval.tick() => {
473 sync_mode_enum = SyncMode::from_atomic_u8(sync_mode.clone())?;
474 match sync_mode_enum {
475 SyncMode::Paused => {
476 let mut pause_interval = tokio::time::interval(Duration::from_secs(1));
477 pause_interval.tick().await;
478 while sync_mode_enum == SyncMode::Paused {
479 pause_interval.tick().await;
480 sync_mode_enum = SyncMode::from_atomic_u8(sync_mode.clone())?;
481 }
482 },
483 SyncMode::Shutdown => {
484 let mut wallet_guard = wallet.write().await;
485 let sync_status = match sync_status(&*wallet_guard).await {
486 Ok(status) => status,
487 Err(SyncStatusError::WalletError(e)) => {
488 return Err(SyncError::WalletError(e));
489 }
490 Err(SyncStatusError::NoSyncData) => {
491 panic!("sync data must exist!");
492 }
493 };
494 wallet_guard
495 .set_save_flag()
496 .map_err(SyncError::WalletError)?;
497 drop(wallet_guard);
498 mempool_handle.abort();
499 fetcher_handle.abort();
500 tracing::info!("Sync successfully shutdown.");
501
502 return Ok(SyncResult {
503 sync_start_height: sync_status.sync_start_height,
504 sync_end_height: (sync_status
505 .scan_ranges
506 .last()
507 .expect("should be non-empty after syncing")
508 .block_range()
509 .end
510 - 1),
511 blocks_scanned: sync_status.session_blocks_scanned,
512 sapling_outputs_scanned: sync_status.session_sapling_outputs_scanned,
513 orchard_outputs_scanned: sync_status.session_orchard_outputs_scanned,
514 percentage_total_outputs_scanned: sync_status.percentage_total_outputs_scanned,
515 });
516 }
517 SyncMode::Running => (),
518 SyncMode::NotRunning => {
519 panic!("sync mode should not be manually set to NotRunning!");
520 },
521 }
522
523 scanner.update(&mut *wallet.write().await, shutdown_mempool.clone(), nullifier_map_limit_exceeded).await?;
524
525 if matches!(scanner.state, ScannerState::Shutdown) {
526 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
528 if is_shutdown(&scanner, unprocessed_mempool_transactions_count.clone())
529 {
530 tracing::info!("Sync successfully shutdown.");
531 break;
532 }
533 }
534 }
535 }
536 }
537
538 let mut wallet_guard = wallet.write().await;
539 let sync_status = match sync_status(&*wallet_guard).await {
540 Ok(status) => status,
541 Err(SyncStatusError::WalletError(e)) => {
542 return Err(SyncError::WalletError(e));
543 }
544 Err(SyncStatusError::NoSyncData) => {
545 panic!("sync data must exist!");
546 }
547 };
548 for transaction in wallet_guard
550 .get_wallet_transactions_mut()
551 .map_err(SyncError::WalletError)?
552 .values_mut()
553 {
554 for note in transaction.sapling_notes.as_mut_slice() {
555 note.refetch_nullifier_ranges = Vec::new();
556 }
557 for note in transaction.orchard_notes.as_mut_slice() {
558 note.refetch_nullifier_ranges = Vec::new();
559 }
560 }
561 wallet_guard
562 .set_save_flag()
563 .map_err(SyncError::WalletError)?;
564
565 drop(wallet_guard);
566 drop(scanner);
567 drop(fetch_request_sender);
568
569 match mempool_handle.await.expect("task panicked") {
570 Ok(()) => (),
571 Err(e @ MempoolError::ShutdownWithoutStream) => tracing::warn!("{e}"),
572 Err(e) => return Err(e.into()),
573 }
574 fetcher_handle.await.expect("task panicked");
575
576 Ok(SyncResult {
577 sync_start_height: sync_status.sync_start_height,
578 sync_end_height: (sync_status
579 .scan_ranges
580 .last()
581 .expect("should be non-empty after syncing")
582 .block_range()
583 .end
584 - 1),
585 blocks_scanned: sync_status.session_blocks_scanned,
586 sapling_outputs_scanned: sync_status.session_sapling_outputs_scanned,
587 orchard_outputs_scanned: sync_status.session_orchard_outputs_scanned,
588 percentage_total_outputs_scanned: sync_status.percentage_total_outputs_scanned,
589 })
590}
591
592fn checked_wallet_height<W, P>(
608 wallet: &mut W,
609 chain_height: BlockHeight,
610 consensus_parameters: &P,
611) -> Result<BlockHeight, SyncError<W::Error>>
612where
613 W: SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
614 P: zcash_protocol::consensus::Parameters,
615{
616 let sync_state = wallet.get_sync_state().map_err(SyncError::WalletError)?;
617 if let Some(last_known_chain_height) = sync_state.last_known_chain_height() {
618 if last_known_chain_height > chain_height {
619 if last_known_chain_height - chain_height >= MAX_REORG_ALLOWANCE {
620 return Err(SyncError::ChainError(
624 u32::from(last_known_chain_height),
625 MAX_REORG_ALLOWANCE,
626 u32::from(chain_height),
627 ));
628 }
629 truncate_wallet_data(wallet, chain_height)?;
632 truncate_scan_ranges(
633 chain_height,
634 wallet
635 .get_sync_state_mut()
636 .map_err(SyncError::WalletError)?,
637 );
638 return Ok(chain_height);
639 }
640 Ok(last_known_chain_height)
642 } else {
643 let sapling_activation_height = consensus_parameters
645 .activation_height(consensus::NetworkUpgrade::Sapling)
646 .expect("sapling activation height should always return Some");
647 let birthday = wallet.get_birthday().map_err(SyncError::WalletError)?;
648 if birthday > chain_height {
649 return Err(SyncError::ChainError(
652 u32::from(birthday),
653 MAX_REORG_ALLOWANCE,
654 u32::from(chain_height),
655 ));
656 } else if birthday < sapling_activation_height {
657 return Err(SyncError::BirthdayBelowSapling(
658 u32::from(birthday),
659 u32::from(sapling_activation_height),
660 ));
661 }
662
663 Ok(birthday - 1)
664 }
665}
666
667pub async fn sync_status<W>(wallet: &W) -> Result<SyncStatus, SyncStatusError<W::Error>>
673where
674 W: SyncWallet + SyncBlocks,
675{
676 let (total_sapling_outputs_scanned, total_orchard_outputs_scanned) =
677 state::calculate_scanned_outputs(wallet).map_err(SyncStatusError::WalletError)?;
678 let total_outputs_scanned = total_sapling_outputs_scanned + total_orchard_outputs_scanned;
679
680 let sync_state = wallet
681 .get_sync_state()
682 .map_err(SyncStatusError::WalletError)?;
683 if sync_state.initial_sync_state.sync_start_height == 0.into() {
684 return Ok(SyncStatus {
685 scan_ranges: sync_state.scan_ranges.clone(),
686 sync_start_height: 0.into(),
687 session_blocks_scanned: 0,
688 total_blocks_scanned: 0,
689 percentage_session_blocks_scanned: 0.0,
690 percentage_total_blocks_scanned: 0.0,
691 session_sapling_outputs_scanned: 0,
692 session_orchard_outputs_scanned: 0,
693 total_sapling_outputs_scanned: 0,
694 total_orchard_outputs_scanned: 0,
695 percentage_session_outputs_scanned: 0.0,
696 percentage_total_outputs_scanned: 0.0,
697 });
698 }
699 let total_blocks_scanned = state::calculate_scanned_blocks(sync_state);
700
701 let birthday = sync_state
702 .wallet_birthday()
703 .ok_or(SyncStatusError::NoSyncData)?;
704 let last_known_chain_height = sync_state
705 .last_known_chain_height()
706 .ok_or(SyncStatusError::NoSyncData)?;
707 let total_blocks = last_known_chain_height - birthday + 1;
708 let total_sapling_outputs = sync_state
709 .initial_sync_state
710 .wallet_tree_bounds
711 .sapling_final_tree_size
712 - sync_state
713 .initial_sync_state
714 .wallet_tree_bounds
715 .sapling_initial_tree_size;
716 let total_orchard_outputs = sync_state
717 .initial_sync_state
718 .wallet_tree_bounds
719 .orchard_final_tree_size
720 - sync_state
721 .initial_sync_state
722 .wallet_tree_bounds
723 .orchard_initial_tree_size;
724 let total_outputs = total_sapling_outputs + total_orchard_outputs;
725
726 let session_blocks_scanned =
727 total_blocks_scanned - sync_state.initial_sync_state.previously_scanned_blocks;
728 let mut percentage_session_blocks_scanned = ((session_blocks_scanned as f32
729 / (total_blocks - sync_state.initial_sync_state.previously_scanned_blocks) as f32)
730 * 100.0)
731 .clamp(0.0, 100.0);
732 let mut percentage_total_blocks_scanned =
733 ((total_blocks_scanned as f32 / total_blocks as f32) * 100.0).clamp(0.0, 100.0);
734
735 let session_sapling_outputs_scanned = total_sapling_outputs_scanned
736 - sync_state
737 .initial_sync_state
738 .previously_scanned_sapling_outputs;
739 let session_orchard_outputs_scanned = total_orchard_outputs_scanned
740 - sync_state
741 .initial_sync_state
742 .previously_scanned_orchard_outputs;
743 let session_outputs_scanned = session_sapling_outputs_scanned + session_orchard_outputs_scanned;
744 let previously_scanned_outputs = sync_state
745 .initial_sync_state
746 .previously_scanned_sapling_outputs
747 + sync_state
748 .initial_sync_state
749 .previously_scanned_orchard_outputs;
750 let mut percentage_session_outputs_scanned = ((session_outputs_scanned as f32
751 / (total_outputs - previously_scanned_outputs) as f32)
752 * 100.0)
753 .clamp(0.0, 100.0);
754 let mut percentage_total_outputs_scanned =
755 ((total_outputs_scanned as f32 / total_outputs as f32) * 100.0).clamp(0.0, 100.0);
756
757 if sync_state.scan_ranges().iter().any(|scan_range| {
758 scan_range.priority() == ScanPriority::ScannedWithoutMapping
759 || scan_range.priority() == ScanPriority::RefetchingNullifiers
760 }) {
761 if percentage_session_blocks_scanned == 100.0 {
762 percentage_session_blocks_scanned = 99.0;
763 }
764 if percentage_total_blocks_scanned == 100.0 {
765 percentage_total_blocks_scanned = 99.0;
766 }
767 if percentage_session_outputs_scanned == 100.0 {
768 percentage_session_outputs_scanned = 99.0;
769 }
770 if percentage_total_outputs_scanned == 100.0 {
771 percentage_total_outputs_scanned = 99.0;
772 }
773 }
774
775 Ok(SyncStatus {
776 scan_ranges: sync_state.scan_ranges.clone(),
777 sync_start_height: sync_state.initial_sync_state.sync_start_height,
778 session_blocks_scanned,
779 total_blocks_scanned,
780 percentage_session_blocks_scanned,
781 percentage_total_blocks_scanned,
782 session_sapling_outputs_scanned,
783 total_sapling_outputs_scanned,
784 session_orchard_outputs_scanned,
785 total_orchard_outputs_scanned,
786 percentage_session_outputs_scanned,
787 percentage_total_outputs_scanned,
788 })
789}
790
791pub fn scan_pending_transaction<W>(
798 consensus_parameters: &impl consensus::Parameters,
799 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
800 wallet: &mut W,
801 transaction: Transaction,
802 status: ConfirmationStatus,
803 datetime: u32,
804) -> Result<(), SyncError<W::Error>>
805where
806 W: SyncWallet + SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
807{
808 if matches!(status, ConfirmationStatus::Confirmed(_)) {
809 panic!("this fn is for unconfirmed transactions only");
810 }
811
812 let mut pending_transaction_nullifiers = NullifierMap::new();
813 let mut pending_transaction_outpoints = BTreeMap::new();
814 let transparent_addresses: HashMap<String, TransparentAddressId> = wallet
815 .get_transparent_addresses()
816 .map_err(SyncError::WalletError)?
817 .iter()
818 .map(|(id, address)| (address.clone(), *id))
819 .collect();
820 let pending_transaction = scan_transaction(
821 consensus_parameters,
822 ufvks,
823 transaction.txid(),
824 transaction,
825 status,
826 None,
827 &mut pending_transaction_nullifiers,
828 &mut pending_transaction_outpoints,
829 &transparent_addresses,
830 datetime,
831 )?;
832
833 let wallet_transactions = wallet
834 .get_wallet_transactions()
835 .map_err(SyncError::WalletError)?;
836 let transparent_output_ids = spend::collect_transparent_output_ids(wallet_transactions);
837 let transparent_spend_scan_targets = spend::detect_transparent_spends(
838 &mut pending_transaction_outpoints,
839 transparent_output_ids,
840 );
841 let (sapling_derived_nullifiers, orchard_derived_nullifiers) =
842 spend::collect_derived_nullifiers(wallet_transactions);
843 let (sapling_spend_scan_targets, orchard_spend_scan_targets) = spend::detect_shielded_spends(
844 &mut pending_transaction_nullifiers,
845 sapling_derived_nullifiers,
846 orchard_derived_nullifiers,
847 );
848
849 if pending_transaction.transparent_coins().is_empty()
851 && pending_transaction.sapling_notes().is_empty()
852 && pending_transaction.orchard_notes().is_empty()
853 && pending_transaction.outgoing_orchard_notes().is_empty()
854 && pending_transaction.outgoing_sapling_notes().is_empty()
855 && transparent_spend_scan_targets.is_empty()
856 && sapling_spend_scan_targets.is_empty()
857 && orchard_spend_scan_targets.is_empty()
858 {
859 return Ok(());
860 }
861
862 wallet
863 .insert_wallet_transaction(pending_transaction)
864 .map_err(SyncError::WalletError)?;
865 spend::update_spent_coins(
866 wallet
867 .get_wallet_transactions_mut()
868 .map_err(SyncError::WalletError)?,
869 transparent_spend_scan_targets,
870 );
871 spend::update_spent_notes(
872 wallet,
873 sapling_spend_scan_targets,
874 orchard_spend_scan_targets,
875 false,
876 )
877 .map_err(SyncError::WalletError)?;
878
879 Ok(())
880}
881
882pub fn add_scan_targets(sync_state: &mut SyncState, scan_targets: &[ScanTarget]) {
893 for scan_target in scan_targets {
894 sync_state.scan_targets.insert(*scan_target);
895 }
896}
897
898pub fn reset_spends(
904 wallet_transactions: &mut HashMap<TxId, WalletTransaction>,
905 invalid_txids: Vec<TxId>,
906) {
907 wallet_transactions
908 .values_mut()
909 .flat_map(|transaction| transaction.orchard_notes_mut())
910 .filter(|output| {
911 output
912 .spending_transaction
913 .is_some_and(|spending_txid| invalid_txids.contains(&spending_txid))
914 })
915 .for_each(|output| {
916 output.set_spending_transaction(None);
917 });
918 wallet_transactions
919 .values_mut()
920 .flat_map(|transaction| transaction.sapling_notes_mut())
921 .filter(|output| {
922 output
923 .spending_transaction
924 .is_some_and(|spending_txid| invalid_txids.contains(&spending_txid))
925 })
926 .for_each(|output| {
927 output.set_spending_transaction(None);
928 });
929 wallet_transactions
930 .values_mut()
931 .flat_map(|transaction| transaction.transparent_coins_mut())
932 .filter(|output| {
933 output
934 .spending_transaction
935 .is_some_and(|spending_txid| invalid_txids.contains(&spending_txid))
936 })
937 .for_each(|output| {
938 output.set_spending_transaction(None);
939 });
940}
941
942pub fn set_transactions_failed(
946 wallet_transactions: &mut HashMap<TxId, WalletTransaction>,
947 failed_txids: Vec<TxId>,
948) {
949 for failed_txid in failed_txids.iter() {
950 if let Some(transaction) = wallet_transactions.get_mut(failed_txid) {
951 let height = transaction.status().get_height();
952 transaction.update_status(
953 ConfirmationStatus::Failed(height),
954 SystemTime::now()
955 .duration_since(SystemTime::UNIX_EPOCH)
956 .expect("infalliable for such long time periods")
957 .as_secs() as u32,
958 );
959 }
960 }
961 reset_spends(wallet_transactions, failed_txids);
962}
963
964fn is_shutdown<P>(
966 scanner: &Scanner<P>,
967 mempool_unprocessed_transactions_count: Arc<AtomicU8>,
968) -> bool
969where
970 P: consensus::Parameters + Sync + Send + 'static,
971{
972 scanner.worker_poolsize() == 0
973 && mempool_unprocessed_transactions_count.load(atomic::Ordering::Acquire) == 0
974}
975
976#[allow(clippy::too_many_arguments)]
978async fn process_scan_results<W>(
979 consensus_parameters: &impl consensus::Parameters,
980 wallet: &mut W,
981 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
982 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
983 scan_range: ScanRange,
984 scan_results: Result<ScanResults, ScanError>,
985 initial_reorg_detection_start_height: BlockHeight,
986 performance_level: PerformanceLevel,
987 nullifier_map_limit_exceeded: &mut bool,
988) -> Result<(), SyncError<W::Error>>
989where
990 W: SyncWallet
991 + SyncBlocks
992 + SyncTransactions
993 + SyncNullifiers
994 + SyncOutPoints
995 + SyncShardTrees
996 + Send,
997{
998 match scan_results {
999 Ok(results) => {
1000 let ScanResults {
1001 mut nullifiers,
1002 mut outpoints,
1003 scanned_blocks,
1004 wallet_transactions,
1005 sapling_located_trees,
1006 orchard_located_trees,
1007 } = results;
1008
1009 if scan_range.priority() == ScanPriority::ScannedWithoutMapping {
1010 let full_refetching_nullifiers_range = wallet
1013 .get_sync_state()
1014 .map_err(SyncError::WalletError)?
1015 .scan_ranges
1016 .iter()
1017 .find(|&wallet_scan_range| {
1018 wallet_scan_range
1019 .block_range()
1020 .contains(&scan_range.block_range().start)
1021 && wallet_scan_range
1022 .block_range()
1023 .contains(&(scan_range.block_range().end - 1))
1024 })
1025 .expect("wallet scan range containing scan range should exist!");
1026 if scan_range.block_range().start
1027 != full_refetching_nullifiers_range.block_range().start
1028 || scan_range.block_range().end
1029 != full_refetching_nullifiers_range.block_range().end
1030 {
1031 let mut missing_block_bounds = BTreeMap::new();
1032 for block_bound in [
1033 scan_range.block_range().start - 1,
1034 scan_range.block_range().start,
1035 scan_range.block_range().end - 1,
1036 scan_range.block_range().end,
1037 ] {
1038 if block_bound < full_refetching_nullifiers_range.block_range().start
1039 || block_bound >= full_refetching_nullifiers_range.block_range().end
1040 {
1041 continue;
1042 }
1043 if wallet.get_wallet_block(block_bound).is_err() {
1044 missing_block_bounds.insert(
1045 block_bound,
1046 WalletBlock::from_compact_block(
1047 consensus_parameters,
1048 fetch_request_sender.clone(),
1049 &client::get_compact_block(
1050 fetch_request_sender.clone(),
1051 block_bound,
1052 )
1053 .await?,
1054 )
1055 .await?,
1056 );
1057 }
1058 }
1059 if !missing_block_bounds.is_empty() {
1060 wallet
1061 .append_wallet_blocks(missing_block_bounds)
1062 .map_err(SyncError::WalletError)?;
1063 }
1064 }
1065
1066 let first_unscanned_range = wallet
1067 .get_sync_state()
1068 .map_err(SyncError::WalletError)?
1069 .scan_ranges
1070 .iter()
1071 .find(|scan_range| scan_range.priority() != ScanPriority::Scanned)
1072 .expect("the scan range being processed is not yet set to scanned so at least one unscanned range must exist");
1073 if !first_unscanned_range
1074 .block_range()
1075 .contains(&scan_range.block_range().start)
1076 || !first_unscanned_range
1077 .block_range()
1078 .contains(&(scan_range.block_range().end - 1))
1079 {
1080 state::reset_refetching_nullifiers_scan_range(
1084 wallet
1085 .get_sync_state_mut()
1086 .map_err(SyncError::WalletError)?,
1087 scan_range.block_range().clone(),
1088 );
1089 tracing::debug!(
1090 "Nullifiers discarded and will be re-fetched to avoid missing spends."
1091 );
1092
1093 return Ok(());
1094 }
1095
1096 spend::update_shielded_spends(
1097 consensus_parameters,
1098 wallet,
1099 fetch_request_sender.clone(),
1100 ufvks,
1101 &scanned_blocks,
1102 Some(&mut nullifiers),
1103 )
1104 .await?;
1105
1106 state::set_scanned_scan_range(
1107 wallet
1108 .get_sync_state_mut()
1109 .map_err(SyncError::WalletError)?,
1110 scan_range.block_range().clone(),
1111 true, );
1113 } else {
1114 if !*nullifier_map_limit_exceeded {
1116 let nullifier_map = wallet.get_nullifiers().map_err(SyncError::WalletError)?;
1117 if max_nullifier_map_size(performance_level).is_some_and(|max| {
1118 nullifier_map.orchard.len()
1119 + nullifier_map.sapling.len()
1120 + nullifiers.orchard.len()
1121 + nullifiers.sapling.len()
1122 > max
1123 }) {
1124 *nullifier_map_limit_exceeded = true;
1125 }
1126 }
1127 let mut map_nullifiers = !*nullifier_map_limit_exceeded;
1128
1129 let map_outpoints = scan_range.priority() >= ScanPriority::FoundNote;
1132
1133 for query_scan_range in wallet
1140 .get_sync_state()
1141 .map_err(SyncError::WalletError)?
1142 .scan_ranges()
1143 {
1144 let scan_priority = query_scan_range.priority();
1145 if scan_priority != ScanPriority::Scanned
1146 && scan_priority != ScanPriority::Scanning
1147 && scan_priority != ScanPriority::RefetchingNullifiers
1148 {
1149 break;
1150 }
1151
1152 if scan_priority == ScanPriority::Scanning
1153 && query_scan_range
1154 .block_range()
1155 .contains(&scan_range.block_range().start)
1156 && query_scan_range
1157 .block_range()
1158 .contains(&(scan_range.block_range().end - 1))
1159 {
1160 map_nullifiers = true;
1161 break;
1162 }
1163 }
1164
1165 update_wallet_data(
1166 consensus_parameters,
1167 wallet,
1168 fetch_request_sender.clone(),
1169 ufvks,
1170 &scan_range,
1171 if map_nullifiers {
1172 Some(&mut nullifiers)
1173 } else {
1174 None
1175 },
1176 if map_outpoints {
1177 Some(&mut outpoints)
1178 } else {
1179 None
1180 },
1181 wallet_transactions,
1182 sapling_located_trees,
1183 orchard_located_trees,
1184 )
1185 .await?;
1186 spend::update_transparent_spends(
1187 wallet,
1188 if map_outpoints {
1189 None
1190 } else {
1191 Some(&mut outpoints)
1192 },
1193 )
1194 .map_err(SyncError::WalletError)?;
1195 spend::update_shielded_spends(
1196 consensus_parameters,
1197 wallet,
1198 fetch_request_sender,
1199 ufvks,
1200 &scanned_blocks,
1201 if map_nullifiers {
1202 None
1203 } else {
1204 Some(&mut nullifiers)
1205 },
1206 )
1207 .await?;
1208 add_scanned_blocks(wallet, scanned_blocks, &scan_range)
1209 .map_err(SyncError::WalletError)?;
1210
1211 state::set_scanned_scan_range(
1212 wallet
1213 .get_sync_state_mut()
1214 .map_err(SyncError::WalletError)?,
1215 scan_range.block_range().clone(),
1216 map_nullifiers,
1217 );
1218 state::merge_scan_ranges(
1219 wallet
1220 .get_sync_state_mut()
1221 .map_err(SyncError::WalletError)?,
1222 ScanPriority::ScannedWithoutMapping,
1223 );
1224 }
1225
1226 state::merge_scan_ranges(
1227 wallet
1228 .get_sync_state_mut()
1229 .map_err(SyncError::WalletError)?,
1230 ScanPriority::Scanned,
1231 );
1232 remove_irrelevant_data(wallet).map_err(SyncError::WalletError)?;
1233 tracing::debug!("Scan results processed.");
1234 }
1235 Err(ScanError::ContinuityError(ContinuityError::HashDiscontinuity { height, .. })) => {
1236 tracing::warn!("Hash discontinuity detected before block {height}.");
1237 if height == scan_range.block_range().start
1238 && scan_range.priority() == ScanPriority::Verify
1239 {
1240 tracing::info!("Re-org detected.");
1241 let sync_state = wallet
1242 .get_sync_state_mut()
1243 .map_err(SyncError::WalletError)?;
1244 let last_known_chain_height = sync_state
1245 .last_known_chain_height()
1246 .expect("scan ranges should be non-empty in this scope");
1247
1248 state::set_scan_priority(
1250 sync_state,
1251 scan_range.block_range(),
1252 ScanPriority::Verify,
1253 );
1254
1255 let current_reorg_detection_start_height = state::set_verify_scan_range(
1257 sync_state,
1258 height - 1,
1259 state::VerifyEnd::VerifyHighest,
1260 )
1261 .block_range()
1262 .start;
1263 state::merge_scan_ranges(sync_state, ScanPriority::Verify);
1264
1265 if initial_reorg_detection_start_height - current_reorg_detection_start_height
1266 > MAX_REORG_ALLOWANCE
1267 {
1268 clear_wallet_data(wallet)?;
1269
1270 return Err(ServerError::ChainVerificationError.into());
1271 }
1272
1273 truncate_wallet_data(wallet, current_reorg_detection_start_height - 1)?;
1274
1275 state::set_initial_state(
1276 consensus_parameters,
1277 fetch_request_sender.clone(),
1278 wallet,
1279 last_known_chain_height,
1280 )
1281 .await?;
1282 } else {
1283 scan_results?;
1284 }
1285 }
1286 Err(e) => return Err(e.into()),
1287 }
1288
1289 Ok(())
1290}
1291
1292async fn process_mempool_transaction<W>(
1296 consensus_parameters: &impl consensus::Parameters,
1297 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
1298 wallet: &mut W,
1299 raw_transaction: RawTransaction,
1300) -> Result<(), SyncError<W::Error>>
1301where
1302 W: SyncWallet + SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
1303{
1304 let mempool_height = wallet
1306 .get_sync_state()
1307 .map_err(SyncError::WalletError)?
1308 .last_known_chain_height()
1309 .expect("wallet height must exist after sync is initialised")
1310 + 1;
1311
1312 let transaction = zcash_primitives::transaction::Transaction::read(
1313 &raw_transaction.data[..],
1314 consensus::BranchId::for_height(consensus_parameters, mempool_height),
1315 )
1316 .map_err(ServerError::InvalidTransaction)?;
1317
1318 tracing::debug!(
1319 "mempool received txid {} at height {}",
1320 transaction.txid(),
1321 mempool_height
1322 );
1323
1324 if let Some(tx) = wallet
1325 .get_wallet_transactions_mut()
1326 .map_err(SyncError::WalletError)?
1327 .get_mut(&transaction.txid())
1328 {
1329 tx.update_status(
1330 ConfirmationStatus::Mempool(mempool_height),
1331 SystemTime::now()
1332 .duration_since(SystemTime::UNIX_EPOCH)
1333 .expect("infalliable for such long time periods")
1334 .as_secs() as u32,
1335 );
1336
1337 return Ok(());
1338 }
1339
1340 scan_pending_transaction(
1341 consensus_parameters,
1342 ufvks,
1343 wallet,
1344 transaction,
1345 ConfirmationStatus::Mempool(mempool_height),
1346 SystemTime::now()
1347 .duration_since(SystemTime::UNIX_EPOCH)
1348 .expect("infalliable for such long time periods")
1349 .as_secs() as u32,
1350 )?;
1351
1352 Ok(())
1353}
1354
1355fn truncate_wallet_data<W>(
1357 wallet: &mut W,
1358 truncate_height: BlockHeight,
1359) -> Result<(), SyncError<W::Error>>
1360where
1361 W: SyncWallet + SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
1362{
1363 let sync_state = wallet
1364 .get_sync_state_mut()
1365 .map_err(SyncError::WalletError)?;
1366 let highest_scanned_height = sync_state
1367 .highest_scanned_height()
1368 .expect("should be non-empty in this scope");
1369 let wallet_birthday = sync_state
1370 .wallet_birthday()
1371 .expect("should be non-empty in this scope");
1372 let checked_truncate_height = match truncate_height.cmp(&wallet_birthday) {
1373 std::cmp::Ordering::Greater | std::cmp::Ordering::Equal => truncate_height,
1374 std::cmp::Ordering::Less => consensus::H0,
1375 };
1376
1377 if checked_truncate_height > highest_scanned_height {
1378 return Ok(());
1379 }
1380
1381 wallet
1382 .truncate_wallet_blocks(checked_truncate_height)
1383 .map_err(SyncError::WalletError)?;
1384 wallet
1385 .truncate_wallet_transactions(checked_truncate_height)
1386 .map_err(SyncError::WalletError)?;
1387 wallet
1388 .truncate_nullifiers(checked_truncate_height)
1389 .map_err(SyncError::WalletError)?;
1390 wallet
1391 .truncate_outpoints(checked_truncate_height)
1392 .map_err(SyncError::WalletError)?;
1393 match wallet.truncate_shard_trees(checked_truncate_height) {
1394 Ok(_) => Ok(()),
1395 Err(SyncError::TruncationError(height, pooltype)) => {
1396 clear_wallet_data(wallet)?;
1397
1398 Err(SyncError::TruncationError(height, pooltype))
1399 }
1400 Err(e) => Err(e),
1401 }?;
1402
1403 Ok(())
1404}
1405
1406fn clear_wallet_data<W>(wallet: &mut W) -> Result<(), SyncError<W::Error>>
1407where
1408 W: SyncWallet + SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees,
1409{
1410 let scan_targets = wallet
1411 .get_wallet_transactions()
1412 .map_err(SyncError::WalletError)?
1413 .values()
1414 .filter_map(|transaction| {
1415 transaction
1416 .status()
1417 .get_confirmed_height()
1418 .map(|height| ScanTarget {
1419 block_height: height,
1420 txid: transaction.txid(),
1421 narrow_scan_area: true,
1422 })
1423 })
1424 .collect::<Vec<_>>();
1425 truncate_wallet_data(wallet, consensus::H0)?;
1426 truncate_scan_ranges(
1427 consensus::H0,
1428 wallet
1429 .get_sync_state_mut()
1430 .map_err(SyncError::WalletError)?,
1431 );
1432 wallet
1433 .get_wallet_transactions_mut()
1434 .map_err(SyncError::WalletError)?
1435 .clear();
1436 let sync_state = wallet
1437 .get_sync_state_mut()
1438 .map_err(SyncError::WalletError)?;
1439 add_scan_targets(sync_state, &scan_targets);
1440 wallet.set_save_flag().map_err(SyncError::WalletError)?;
1441
1442 Ok(())
1443}
1444
1445#[allow(clippy::too_many_arguments)]
1447async fn update_wallet_data<W>(
1448 consensus_parameters: &impl consensus::Parameters,
1449 wallet: &mut W,
1450 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
1451 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
1452 scan_range: &ScanRange,
1453 nullifiers: Option<&mut NullifierMap>,
1454 outpoints: Option<&mut BTreeMap<OutputId, ScanTarget>>,
1455 mut transactions: HashMap<TxId, WalletTransaction>,
1456 sapling_located_trees: Vec<LocatedTreeData<sapling_crypto::Node>>,
1457 orchard_located_trees: Vec<LocatedTreeData<MerkleHashOrchard>>,
1458) -> Result<(), SyncError<W::Error>>
1459where
1460 W: SyncBlocks + SyncTransactions + SyncNullifiers + SyncOutPoints + SyncShardTrees + Send,
1461{
1462 let sync_state = wallet
1463 .get_sync_state_mut()
1464 .map_err(SyncError::WalletError)?;
1465 let highest_scanned_height = sync_state
1466 .highest_scanned_height()
1467 .expect("scan ranges should not be empty in this scope");
1468 for transaction in transactions.values() {
1469 state::update_found_note_shard_priority(
1470 consensus_parameters,
1471 sync_state,
1472 ShieldedProtocol::Sapling,
1473 transaction,
1474 );
1475 state::update_found_note_shard_priority(
1476 consensus_parameters,
1477 sync_state,
1478 ShieldedProtocol::Orchard,
1479 transaction,
1480 );
1481 }
1482 let refetch_nullifier_ranges = {
1490 let block_ranges: Vec<Range<BlockHeight>> = sync_state
1491 .scan_ranges()
1492 .iter()
1493 .filter(|&scan_range| {
1494 scan_range.priority() == ScanPriority::ScannedWithoutMapping
1495 || scan_range.priority() == ScanPriority::RefetchingNullifiers
1496 })
1497 .map(|scan_range| scan_range.block_range().clone())
1498 .collect();
1499
1500 block_ranges
1501 [block_ranges.partition_point(|range| range.start < scan_range.block_range().end)..]
1502 .to_vec()
1503 };
1504 for transaction in transactions.values_mut() {
1505 for note in transaction.sapling_notes.as_mut_slice() {
1506 note.refetch_nullifier_ranges = refetch_nullifier_ranges.clone();
1507 }
1508 for note in transaction.orchard_notes.as_mut_slice() {
1509 note.refetch_nullifier_ranges = refetch_nullifier_ranges.clone();
1510 }
1511 }
1512 for transaction in transactions.values() {
1513 discover_unified_addresses(wallet, ufvks, transaction).map_err(SyncError::WalletError)?;
1514 }
1515
1516 wallet
1517 .extend_wallet_transactions(transactions)
1518 .map_err(SyncError::WalletError)?;
1519 if let Some(nullifiers) = nullifiers {
1520 wallet
1521 .append_nullifiers(nullifiers)
1522 .map_err(SyncError::WalletError)?;
1523 }
1524 if let Some(outpoints) = outpoints {
1525 wallet
1526 .append_outpoints(outpoints)
1527 .map_err(SyncError::WalletError)?;
1528 }
1529 wallet
1530 .update_shard_trees(
1531 fetch_request_sender,
1532 scan_range,
1533 highest_scanned_height,
1534 sapling_located_trees,
1535 orchard_located_trees,
1536 )
1537 .await?;
1538
1539 Ok(())
1540}
1541
1542fn discover_unified_addresses<W>(
1543 wallet: &mut W,
1544 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
1545 transaction: &WalletTransaction,
1546) -> Result<(), W::Error>
1547where
1548 W: SyncWallet,
1549{
1550 for note in transaction
1551 .orchard_notes()
1552 .iter()
1553 .filter(|¬e| note.key_id().scope == zip32::Scope::External)
1554 {
1555 let ivk = ufvks
1556 .get(¬e.key_id().account_id())
1557 .expect("ufvk must exist to decrypt this note")
1558 .orchard()
1559 .expect("fvk must exist to decrypt this note")
1560 .to_ivk(zip32::Scope::External);
1561
1562 wallet.add_orchard_address(
1563 note.key_id().account_id(),
1564 note.note().recipient(),
1565 ivk.diversifier_index(¬e.note().recipient())
1566 .expect("must be key used to create this address"),
1567 )?;
1568 }
1569 for note in transaction
1570 .sapling_notes()
1571 .iter()
1572 .filter(|¬e| note.key_id().scope == zip32::Scope::External)
1573 {
1574 let ivk = ufvks
1575 .get(¬e.key_id().account_id())
1576 .expect("ufvk must exist to decrypt this note")
1577 .sapling()
1578 .expect("fvk must exist to decrypt this note")
1579 .to_external_ivk();
1580
1581 wallet.add_sapling_address(
1582 note.key_id().account_id(),
1583 note.note().recipient(),
1584 ivk.decrypt_diversifier(¬e.note().recipient())
1585 .expect("must be key used to create this address"),
1586 )?;
1587 }
1588
1589 Ok(())
1590}
1591
1592fn remove_irrelevant_data<W>(wallet: &mut W) -> Result<(), W::Error>
1593where
1594 W: SyncWallet + SyncBlocks + SyncOutPoints + SyncNullifiers + SyncTransactions,
1595{
1596 let fully_scanned_height = wallet
1597 .get_sync_state()?
1598 .fully_scanned_height()
1599 .expect("scan ranges must be non-empty");
1600
1601 wallet
1602 .get_outpoints_mut()?
1603 .retain(|_, scan_target| scan_target.block_height > fully_scanned_height);
1604 wallet
1605 .get_nullifiers_mut()?
1606 .sapling
1607 .retain(|_, scan_target| scan_target.block_height > fully_scanned_height);
1608 wallet
1609 .get_nullifiers_mut()?
1610 .orchard
1611 .retain(|_, scan_target| scan_target.block_height > fully_scanned_height);
1612 wallet
1613 .get_sync_state_mut()?
1614 .scan_targets
1615 .retain(|scan_target| scan_target.block_height > fully_scanned_height);
1616 remove_irrelevant_blocks(wallet)?;
1617
1618 Ok(())
1619}
1620
1621fn remove_irrelevant_blocks<W>(wallet: &mut W) -> Result<(), W::Error>
1622where
1623 W: SyncWallet + SyncBlocks + SyncTransactions,
1624{
1625 let sync_state = wallet.get_sync_state()?;
1626 let highest_scanned_height = sync_state
1627 .highest_scanned_height()
1628 .expect("should be non-empty");
1629 let scanned_range_bounds = sync_state
1630 .scan_ranges()
1631 .iter()
1632 .filter(|scan_range| {
1633 scan_range.priority() == ScanPriority::Scanned
1634 || scan_range.priority() == ScanPriority::ScannedWithoutMapping
1635 || scan_range.priority() == ScanPriority::RefetchingNullifiers
1636 })
1637 .flat_map(|scanned_range| {
1638 vec![
1639 scanned_range.block_range().start,
1640 scanned_range.block_range().end - 1,
1641 ]
1642 })
1643 .collect::<Vec<_>>();
1644 let wallet_transaction_heights = wallet
1645 .get_wallet_transactions()?
1646 .values()
1647 .filter_map(|tx| tx.status().get_confirmed_height())
1648 .collect::<Vec<_>>();
1649
1650 wallet.get_wallet_blocks_mut()?.retain(|height, _| {
1651 *height >= highest_scanned_height.saturating_sub(MAX_REORG_ALLOWANCE)
1652 || scanned_range_bounds.contains(height)
1653 || wallet_transaction_heights.contains(height)
1654 });
1655
1656 Ok(())
1657}
1658
1659fn add_scanned_blocks<W>(
1660 wallet: &mut W,
1661 mut scanned_blocks: BTreeMap<BlockHeight, WalletBlock>,
1662 scan_range: &ScanRange,
1663) -> Result<(), W::Error>
1664where
1665 W: SyncWallet + SyncBlocks + SyncTransactions,
1666{
1667 let sync_state = wallet.get_sync_state()?;
1668 let highest_scanned_height = sync_state
1669 .highest_scanned_height()
1670 .expect("scan ranges must be non-empty");
1671
1672 let wallet_transaction_heights = wallet
1673 .get_wallet_transactions()?
1674 .values()
1675 .filter_map(|tx| tx.status().get_confirmed_height())
1676 .collect::<Vec<_>>();
1677
1678 scanned_blocks.retain(|height, _| {
1679 *height >= highest_scanned_height.saturating_sub(MAX_REORG_ALLOWANCE)
1680 || *height == scan_range.block_range().start
1681 || *height == scan_range.block_range().end - 1
1682 || wallet_transaction_heights.contains(height)
1683 });
1684
1685 wallet.append_wallet_blocks(scanned_blocks)?;
1686
1687 Ok(())
1688}
1689
1690#[cfg(not(feature = "darkside_test"))]
1691async fn update_subtree_roots<W>(
1692 consensus_parameters: &impl consensus::Parameters,
1693 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
1694 wallet: &mut W,
1695) -> Result<(), SyncError<W::Error>>
1696where
1697 W: SyncWallet + SyncShardTrees,
1698{
1699 let sapling_start_index = wallet
1700 .get_shard_trees()
1701 .map_err(SyncError::WalletError)?
1702 .sapling
1703 .store()
1704 .get_shard_roots()
1705 .expect("infallible")
1706 .len() as u32;
1707 let orchard_start_index = wallet
1708 .get_shard_trees()
1709 .map_err(SyncError::WalletError)?
1710 .orchard
1711 .store()
1712 .get_shard_roots()
1713 .expect("infallible")
1714 .len() as u32;
1715 let (sapling_subtree_roots, orchard_subtree_roots) = futures::join!(
1716 client::get_subtree_roots(fetch_request_sender.clone(), sapling_start_index, 0, 0),
1717 client::get_subtree_roots(fetch_request_sender, orchard_start_index, 1, 0)
1718 );
1719
1720 let sapling_subtree_roots = sapling_subtree_roots?;
1721 let orchard_subtree_roots = orchard_subtree_roots?;
1722
1723 let sync_state = wallet
1724 .get_sync_state_mut()
1725 .map_err(SyncError::WalletError)?;
1726 state::add_shard_ranges(
1727 consensus_parameters,
1728 ShieldedProtocol::Sapling,
1729 sync_state,
1730 &sapling_subtree_roots,
1731 );
1732 state::add_shard_ranges(
1733 consensus_parameters,
1734 ShieldedProtocol::Orchard,
1735 sync_state,
1736 &orchard_subtree_roots,
1737 );
1738
1739 let shard_trees = wallet
1740 .get_shard_trees_mut()
1741 .map_err(SyncError::WalletError)?;
1742 witness::add_subtree_roots(
1743 sapling_start_index as usize,
1744 sapling_subtree_roots,
1745 &mut shard_trees.sapling,
1746 )?;
1747 witness::add_subtree_roots(
1748 orchard_start_index as usize,
1749 orchard_subtree_roots,
1750 &mut shard_trees.orchard,
1751 )?;
1752
1753 Ok(())
1754}
1755
1756async fn add_initial_frontier<W>(
1757 consensus_parameters: &impl consensus::Parameters,
1758 fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
1759 wallet: &mut W,
1760) -> Result<(), SyncError<W::Error>>
1761where
1762 W: SyncWallet + SyncShardTrees,
1763{
1764 let birthday = wallet.get_birthday().map_err(SyncError::WalletError)?;
1765 if birthday
1766 == consensus_parameters
1767 .activation_height(consensus::NetworkUpgrade::Sapling)
1768 .expect("sapling activation height should always return Some")
1769 {
1770 return Ok(());
1771 }
1772
1773 let shard_trees = wallet
1776 .get_shard_trees_mut()
1777 .map_err(SyncError::WalletError)?;
1778 if shard_trees
1779 .sapling
1780 .store()
1781 .checkpoint_count()
1782 .expect("infallible")
1783 == 1
1784 {
1785 let frontiers = client::get_frontiers(fetch_request_sender, birthday).await?;
1786 shard_trees
1787 .sapling
1788 .insert_frontier(
1789 frontiers.final_sapling_tree().clone(),
1790 Retention::Checkpoint {
1791 id: birthday,
1792 marking: Marking::None,
1793 },
1794 )
1795 .expect("infallible");
1796 shard_trees
1797 .orchard
1798 .insert_frontier(
1799 frontiers.final_orchard_tree().clone(),
1800 Retention::Checkpoint {
1801 id: birthday,
1802 marking: Marking::None,
1803 },
1804 )
1805 .expect("infallible");
1806 }
1807
1808 Ok(())
1809}
1810
1811async fn mempool_monitor(
1816 mut client: CompactTxStreamerClient<Channel>,
1817 mempool_transaction_sender: mpsc::Sender<RawTransaction>,
1818 unprocessed_transactions_count: Arc<AtomicU8>,
1819 shutdown_mempool: Arc<AtomicBool>,
1820) -> Result<(), MempoolError> {
1821 let mut interval = tokio::time::interval(Duration::from_secs(1));
1822 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1823 'main: loop {
1824 let response =
1825 client::get_mempool_transaction_stream(&mut client, shutdown_mempool.clone()).await;
1826
1827 match response {
1828 Ok(mut mempool_stream) => {
1829 interval.reset();
1830 loop {
1831 tokio::select! {
1832 mempool_stream_message = mempool_stream.message() => {
1833 match mempool_stream_message.unwrap_or(None) {
1834 Some(raw_transaction) => {
1835 let _ignore_error = mempool_transaction_sender
1836 .send(raw_transaction)
1837 .await;
1838 unprocessed_transactions_count.fetch_add(1, atomic::Ordering::Release);
1839 }
1840 None => {
1841 continue 'main;
1842 }
1843 }
1844
1845 }
1846
1847 _ = interval.tick() => {
1848 if shutdown_mempool.load(atomic::Ordering::Acquire) {
1849 break 'main;
1850 }
1851 }
1852 }
1853 }
1854 }
1855 Err(e @ MempoolError::ShutdownWithoutStream) => return Err(e),
1856 Err(MempoolError::ServerError(e)) => {
1857 tracing::warn!("Mempool stream request failed! Status: {e}.\nRetrying...");
1858 tokio::time::sleep(Duration::from_secs(3)).await;
1859 }
1860 }
1861 }
1862
1863 Ok(())
1864}
1865
1866fn expire_transactions<W>(wallet: &mut W) -> Result<(), SyncError<W::Error>>
1871where
1872 W: SyncWallet + SyncTransactions,
1873{
1874 let last_known_chain_height = wallet
1875 .get_sync_state()
1876 .map_err(SyncError::WalletError)?
1877 .last_known_chain_height()
1878 .expect("wallet height must exist after scan ranges have been updated");
1879 let wallet_transactions = wallet
1880 .get_wallet_transactions_mut()
1881 .map_err(SyncError::WalletError)?;
1882
1883 let expired_txids = wallet_transactions
1884 .values()
1885 .filter(|transaction| {
1886 transaction.status().is_pending()
1887 && last_known_chain_height >= transaction.transaction().expiry_height()
1888 })
1889 .map(super::wallet::WalletTransaction::txid)
1890 .collect::<Vec<_>>();
1891 set_transactions_failed(wallet_transactions, expired_txids);
1892
1893 let stuck_funds_txids = wallet_transactions
1894 .values()
1895 .filter(|transaction| {
1896 transaction.status().is_pending()
1897 && last_known_chain_height
1898 >= transaction.status().get_height() + UNCONFIRMED_SPEND_INVALIDATION_THRESHOLD
1899 })
1900 .map(super::wallet::WalletTransaction::txid)
1901 .collect::<Vec<_>>();
1902 reset_spends(wallet_transactions, stuck_funds_txids);
1903
1904 Ok(())
1905}
1906
1907fn max_nullifier_map_size(performance_level: PerformanceLevel) -> Option<usize> {
1908 match performance_level {
1909 PerformanceLevel::Low => Some(0),
1910 PerformanceLevel::Medium => Some(125_000),
1911 PerformanceLevel::High => Some(2_000_000),
1912 PerformanceLevel::Maximum => None,
1913 }
1914}
1915
1916#[cfg(test)]
1917mod test {
1918
1919 mod checked_height_validation {
1920 use zcash_protocol::consensus::BlockHeight;
1921 use zcash_protocol::local_consensus::LocalNetwork;
1922 const LOCAL_NETWORK: LocalNetwork = LocalNetwork {
1923 overwinter: Some(BlockHeight::from_u32(1)),
1924 sapling: Some(BlockHeight::from_u32(3)),
1925 blossom: Some(BlockHeight::from_u32(3)),
1926 heartwood: Some(BlockHeight::from_u32(3)),
1927 canopy: Some(BlockHeight::from_u32(3)),
1928 nu5: Some(BlockHeight::from_u32(3)),
1929 nu6: Some(BlockHeight::from_u32(3)),
1930 nu6_1: Some(BlockHeight::from_u32(3)),
1931 nu6_2: Some(BlockHeight::from_u32(3)),
1932 };
1933 use crate::{error::SyncError, mocks::MockWalletError, sync::checked_wallet_height};
1934 #[tokio::test]
1937 async fn get_sync_state_error() {
1938 let builder = crate::mocks::MockWalletBuilder::new();
1939 let test_error = "get_sync_state_error";
1940 let mut test_wallet = builder
1941 .get_sync_state_patch(Box::new(|_| {
1942 Err(MockWalletError::AnErrorVariant(test_error.to_string()))
1943 }))
1944 .create_mock_wallet();
1945 let res =
1946 checked_wallet_height(&mut test_wallet, BlockHeight::from_u32(1), &LOCAL_NETWORK);
1947 assert!(matches!(
1948 res,
1949 Err(SyncError::WalletError(
1950 crate::mocks::MockWalletError::AnErrorVariant(ref s)
1951 )) if s == test_error
1952 ));
1953 }
1954
1955 mod last_known_chain_height {
1956 use crate::{
1957 sync::{MAX_REORG_ALLOWANCE, ScanRange},
1958 wallet::SyncState,
1959 };
1960 const DEFAULT_START_HEIGHT: BlockHeight = BlockHeight::from_u32(1);
1961 const _DEFAULT_LAST_KNOWN_HEIGHT: BlockHeight = BlockHeight::from_u32(102);
1962 const DEFAULT_CHAIN_HEIGHT: BlockHeight = BlockHeight::from_u32(110);
1963
1964 use super::*;
1965 #[tokio::test]
1966 async fn above_allowance() {
1967 const LAST_KNOWN_HEIGHT: BlockHeight = BlockHeight::from_u32(211);
1968 let lkch = vec![ScanRange::from_parts(
1969 DEFAULT_START_HEIGHT..LAST_KNOWN_HEIGHT,
1970 crate::sync::ScanPriority::Scanned,
1971 )];
1972 let state = SyncState {
1973 scan_ranges: lkch,
1974 ..Default::default()
1975 };
1976 let builder = crate::mocks::MockWalletBuilder::new();
1977 let mut test_wallet = builder.sync_state(state).create_mock_wallet();
1978 let res =
1979 checked_wallet_height(&mut test_wallet, DEFAULT_CHAIN_HEIGHT, &LOCAL_NETWORK);
1980 if let Err(e) = res {
1981 assert_eq!(
1982 e.to_string(),
1983 format!(
1984 "wallet height {} is more than {} blocks ahead of best chain height {}",
1985 LAST_KNOWN_HEIGHT - 1,
1986 MAX_REORG_ALLOWANCE,
1987 DEFAULT_CHAIN_HEIGHT
1988 )
1989 );
1990 } else {
1991 panic!()
1992 }
1993 }
1994 #[tokio::test]
1995 async fn above_chain_height_below_allowance() {
1996 let lkch = vec![ScanRange::from_parts(
2000 BlockHeight::from_u32(6)..BlockHeight::from_u32(10),
2001 crate::sync::ScanPriority::Scanned,
2002 )];
2003 let state = SyncState {
2004 scan_ranges: lkch,
2005 ..Default::default()
2006 };
2007 let builder = crate::mocks::MockWalletBuilder::new();
2008 let mut test_wallet = builder.sync_state(state).create_mock_wallet();
2009 let chain_height = BlockHeight::from_u32(4);
2010 let res = checked_wallet_height(&mut test_wallet, chain_height, &LOCAL_NETWORK);
2015 assert_eq!(res.unwrap(), BlockHeight::from_u32(4));
2016 }
2017 #[ignore = "in progress"]
2018 #[tokio::test]
2019 async fn equal_or_below_chain_height_and_above_sapling() {
2020 let lkch = vec![ScanRange::from_parts(
2021 BlockHeight::from_u32(1)..BlockHeight::from_u32(10),
2022 crate::sync::ScanPriority::Scanned,
2023 )];
2024 let state = SyncState {
2025 scan_ranges: lkch,
2026 ..Default::default()
2027 };
2028 let builder = crate::mocks::MockWalletBuilder::new();
2029 let mut _test_wallet = builder.sync_state(state).create_mock_wallet();
2030 }
2031 #[ignore = "in progress"]
2032 #[tokio::test]
2033 async fn equal_or_below_chain_height_and_below_sapling() {
2034 let lkch = vec![ScanRange::from_parts(
2037 BlockHeight::from_u32(1)..BlockHeight::from_u32(10),
2038 crate::sync::ScanPriority::Scanned,
2039 )];
2040 let state = SyncState {
2041 scan_ranges: lkch,
2042 ..Default::default()
2043 };
2044 let builder = crate::mocks::MockWalletBuilder::new();
2045 let mut _test_wallet = builder.sync_state(state).create_mock_wallet();
2046 }
2047 #[ignore = "in progress"]
2048 #[tokio::test]
2049 async fn below_sapling() {
2050 let lkch = vec![ScanRange::from_parts(
2051 BlockHeight::from_u32(1)..BlockHeight::from_u32(10),
2052 crate::sync::ScanPriority::Scanned,
2053 )];
2054 let state = SyncState {
2055 scan_ranges: lkch,
2056 ..Default::default()
2057 };
2058 let builder = crate::mocks::MockWalletBuilder::new();
2059 let mut _test_wallet = builder.sync_state(state).create_mock_wallet();
2060 }
2061 }
2062 mod no_last_known_chain_height {
2063 use super::*;
2064 #[tokio::test]
2066 async fn get_bday_error() {
2067 let test_error = "get_bday_error";
2068 let builder = crate::mocks::MockWalletBuilder::new();
2069 let mut test_wallet = builder
2070 .get_birthday_patch(Box::new(|_| {
2071 Err(crate::mocks::MockWalletError::AnErrorVariant(
2072 test_error.to_string(),
2073 ))
2074 }))
2075 .create_mock_wallet();
2076 let res = checked_wallet_height(
2077 &mut test_wallet,
2078 BlockHeight::from_u32(1),
2079 &LOCAL_NETWORK,
2080 );
2081 assert!(matches!(
2082 res,
2083 Err(SyncError::WalletError(
2084 crate::mocks::MockWalletError::AnErrorVariant(ref s)
2085 )) if s == test_error
2086 ));
2087 }
2088 #[ignore = "in progress"]
2089 #[tokio::test]
2090 async fn raw_bday_above_chain_height() {
2091 let builder = crate::mocks::MockWalletBuilder::new();
2092 let mut test_wallet = builder
2093 .birthday(BlockHeight::from_u32(15))
2094 .create_mock_wallet();
2095 let res = checked_wallet_height(
2096 &mut test_wallet,
2097 BlockHeight::from_u32(1),
2098 &LOCAL_NETWORK,
2099 );
2100 if let Err(e) = res {
2101 assert_eq!(
2102 e.to_string(),
2103 format!(
2104 "wallet height is more than {} blocks ahead of best chain height",
2105 15 - 1
2106 )
2107 );
2108 } else {
2109 panic!()
2110 }
2111 }
2112 mod sapling_height {
2113 use super::*;
2114 #[tokio::test]
2115 async fn raw_bday_above() {
2116 let builder = crate::mocks::MockWalletBuilder::new();
2117 let mut test_wallet = builder
2118 .birthday(BlockHeight::from_u32(4))
2119 .create_mock_wallet();
2120 let res = checked_wallet_height(
2121 &mut test_wallet,
2122 BlockHeight::from_u32(5),
2123 &LOCAL_NETWORK,
2124 );
2125 assert_eq!(res.unwrap(), BlockHeight::from_u32(4 - 1));
2126 }
2127 #[tokio::test]
2128 async fn raw_bday_equal() {
2129 let builder = crate::mocks::MockWalletBuilder::new();
2130 let mut test_wallet = builder
2131 .birthday(BlockHeight::from_u32(3))
2132 .create_mock_wallet();
2133 let res = checked_wallet_height(
2134 &mut test_wallet,
2135 BlockHeight::from_u32(5),
2136 &LOCAL_NETWORK,
2137 );
2138 assert_eq!(res.unwrap(), BlockHeight::from_u32(3 - 1));
2139 }
2140 #[tokio::test]
2141 async fn raw_bday_below() {
2142 let builder = crate::mocks::MockWalletBuilder::new();
2143 let mut test_wallet = builder
2144 .birthday(BlockHeight::from_u32(1))
2145 .create_mock_wallet();
2146 let res = checked_wallet_height(
2147 &mut test_wallet,
2148 BlockHeight::from_u32(5),
2149 &LOCAL_NETWORK,
2150 );
2151 assert!(matches!(res, Err(SyncError::BirthdayBelowSapling(1, 3))));
2152 }
2153 }
2154 }
2155 }
2156}