Skip to main content

pepper_sync/
sync.rs

1//! Entrypoint for sync engine
2
3use 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/// A snapshot of the current state of sync. Useful for displaying the status of sync to a user / consumer.
59///
60/// `percentage_outputs_scanned` is a much more accurate indicator of sync completion than `percentage_blocks_scanned`.
61/// `percentage_total_outputs_scanned` is the percentage of outputs scanned from birthday to chain height.
62#[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
79// TODO: complete display, scan ranges in raw form are too verbose
80impl 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/// Returned when [`crate::sync::sync`] successfully completes.
122#[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/// Scanning range priority levels.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
171pub enum ScanPriority {
172    /// Block ranges that are currently refetching nullifiers.
173    RefetchingNullifiers,
174    /// Block ranges that are currently being scanned.
175    Scanning,
176    /// Block ranges that have already been scanned will not be re-scanned.
177    Scanned,
178    /// Block ranges that have already been scanned. The nullifiers from this range were not mapped after scanning and
179    /// spend detection to reduce memory consumption and/or storage for non-linear scanning. These nullifiers will need
180    /// to be re-fetched for final spend detection when this range is the lowest unscanned range in the wallet's list
181    /// of scan ranges.
182    ScannedWithoutMapping,
183    /// Block ranges to be scanned to advance the fully-scanned height.
184    Historic,
185    /// Block ranges adjacent to heights at which the user opened the wallet.
186    OpenAdjacent,
187    /// Blocks that must be scanned to complete note commitment tree shards adjacent to found notes.
188    FoundNote,
189    /// Blocks that must be scanned to complete the latest note commitment tree shard.
190    ChainTip,
191    /// A previously scanned range that must be verified to check it is still in the
192    /// main chain, has highest priority.
193    Verify,
194}
195
196/// A range of blocks to be scanned, along with its associated priority.
197#[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    /// Constructs a scan range from its constituent parts.
215    #[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    /// Returns the range of block heights to be scanned.
228    #[must_use]
229    pub fn block_range(&self) -> &Range<BlockHeight> {
230        &self.block_range
231    }
232
233    /// Returns the priority with which the scan range should be scanned.
234    #[must_use]
235    pub fn priority(&self) -> ScanPriority {
236        self.priority
237    }
238
239    /// Returns whether or not the scan range is empty.
240    #[must_use]
241    pub fn is_empty(&self) -> bool {
242        self.block_range.is_empty()
243    }
244
245    /// Returns the number of blocks in the scan range.
246    #[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    /// Shifts the start of the block range to the right if `block_height >
253    /// self.block_range().start`. Returns `None` if the resulting range would
254    /// be empty (or the range was already empty).
255    #[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    /// Shifts the end of the block range to the left if `block_height <
268    /// self.block_range().end`. Returns `None` if the resulting range would
269    /// be empty (or the range was already empty).
270    #[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    /// Splits this scan range at the specified height, such that the provided height becomes the
283    /// end of the first range returned and the start of the second. Returns `None` if
284    /// `p <= self.block_range().start || p >= self.block_range().end`.
285    #[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
300/// Syncs a wallet to the latest state of the blockchain.
301///
302/// `sync_mode` is intended to be stored in a struct that owns the wallet(s) (i.e. lightclient) and has a non-atomic
303/// counterpart [`crate::wallet::SyncMode`]. The sync engine will set the `sync_mode` to `Running` at the start of sync.
304/// However, the consumer is required to set the `sync_mode` back to `NotRunning` when sync is succussful or returns an
305/// error. This allows more flexibility and safety with sync task handles etc.
306/// `sync_mode` may also be set to `Paused` externally to pause scanning so the wallet lock can be acquired multiple
307/// times in quick sucession without the sync engine interrupting.
308/// Set `sync_mode` back to `Running` to resume scanning.
309/// Set `sync_mode` to `Shutdown` to stop the sync process.
310pub 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    // create channel for sending fetch requests and launch fetcher task
338    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    // create channel for receiving mempool transactions and launch mempool monitor
346    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    // pre-scan initialisation
363    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    // create channel for receiving scan results and launch scanner
425    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    // TODO: implement an option for continuous scanning where it doesnt exit when complete
435
436    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                    // wait for mempool monitor to receive mempool transactions
527                    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    // once sync is complete, all nullifiers will have been re-fetched so this note metadata can be discarded.
549    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
592/// This ensures that the wallet height used to calculate the lower bound for scan range creation is valid.
593/// The comparison takes two input heights and uses several constants to select the correct height.
594///
595/// The input parameter heights are:
596///
597///   (1) chain_height:
598///       * the best block-height reported by the proxy (zainod or lwd)
599///   (2) last_known_chain_height
600///       * the last max height the wallet recorded from earlier scans
601///
602/// The constants are:
603///   (1) MAX_REORG_ALLOWANCE:
604///       * the maximum number of blocks the wallet can truncate during re-org detection
605///   (2) Sapling Activation Height:
606///       * the lower bound on the wallet birthday
607fn 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                // There's a human attention requiring problem, the wallet supplied
621                // last_known_chain_height is more than MAX_REORG_ALLOWANCE **above**
622                // the proxy's reported height.
623                return Err(SyncError::ChainError(
624                    u32::from(last_known_chain_height),
625                    MAX_REORG_ALLOWANCE,
626                    u32::from(chain_height),
627                ));
628            }
629            // The wallet reported height is above the current proxy height
630            // reset to the proxy height.
631            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        // The last wallet reported height is equal or below the proxy height.
641        Ok(last_known_chain_height)
642    } else {
643        // This is the wallet's first sync. Use [birthday - 1] as wallet height.
644        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            // Human attention requiring error, a birthday *above* the proxy reported
650            // chain height has been provided.
651            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
667/// Creates a [`self::SyncStatus`] from the wallet's current [`crate::wallet::SyncState`].
668/// If there is still nullifiers to be re-fetched when scanning is complete, the percentages will be overrided to 99%
669/// until sync is complete.
670///
671/// Intended to be called while [`self::sync`] is running in a separate task.
672pub 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
791/// Scans a pending `transaction` of a given `status`, adding to the wallet and updating output spend statuses.
792///
793/// Used both internally for scanning mempool transactions and externally for scanning calculated and transmitted
794/// transactions during send.
795///
796/// Panics if `status` is of `Confirmed` variant.
797pub 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    // return if transaction is not relevant to the wallet
850    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
882/// API for targetted scanning.
883///
884/// Allows `scan_targets` to be added externally to the wallet's `sync_state` and be prioritised for scanning. Each
885/// scan target must include the block height which will be used to prioritise the block range containing the note
886/// commitments to the surrounding orchard shard(s). If the block height is pre-orchard then the surrounding sapling
887/// shard(s) will be prioritised instead. The txid in each scan target may be omitted and set to [0u8; 32] in order to
888/// prioritise the surrounding blocks for scanning but be ignored when fetching specific relevant transactions to the
889/// wallet. However, in the case where a relevant spending transaction at a given height contains no decryptable
890/// incoming notes (change), only the nullifier will be mapped and this transaction will be scanned when the
891/// transaction containing the spent notes is scanned instead.
892pub 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
898/// Resets the spending transaction field of all outputs that were previously spent but became unspent due to a
899/// spending transactions becoming invalid.
900///
901/// `invalid_txids` are the id's of the invalidated spending transactions. Any outputs in the `wallet_transactions`
902/// matching these spending transactions will be reset back to `None`.
903pub 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
942/// Sets transactions associated with list of `failed_txids` in `wallet_transactions` to `Failed` status.
943///
944/// Sets the `spending_transaction` fields of any outputs spent in these transactions to `None`.
945pub 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
964/// Returns true if the scanner and mempool are shutdown.
965fn 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/// Scan post-processing
977#[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                // add missing block bounds in the case that nullifier batch limit was reached and the fetch nullifier
1011                // scan range was split.
1012                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                    // in this rare edge case, a scanned `ScannedWithoutMapping` range was the highest priority yet it was not the first unscanned range so it must be discarded to avoid missing spends
1081
1082                    // reset scan range from `RefetchingNullifiers` to `ScannedWithoutMapping`
1083                    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, // NOTE: although nullifiers are not actually added to the wallet's nullifier map for efficiency, there is effectively no difference as spends are still updated using the `additional_nullifier_map` and would be removed on the following cleanup (`remove_irrelevant_data`) due to `ScannedWithoutMapping` ranges always being the first non-scanned range and therefore always raise the wallet's fully scanned height after processing.
1112                );
1113            } else {
1114                // nullifiers are not mapped if nullifier map size limit will be exceeded
1115                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                // all transparent spend locations are known before scanning so there is no need to map outpoints from untargetted ranges.
1130                // outpoints of untargetted ranges will still be checked before being discarded.
1131                let map_outpoints = scan_range.priority() >= ScanPriority::FoundNote;
1132
1133                // always map nullifiers if scanning the lowest range to be scanned for final spend detection.
1134                // this will set the range to `Scanned` (as oppose to `ScannedWithoutMapping`) and prevent immediate
1135                // re-fetching of the nullifiers in this range. these will be immediately cleared after cleanup so will not
1136                // have an impact on memory or wallet file size.
1137                // the selected range is not the lowest range to be scanned unless all ranges before it are scanned or
1138                // scanning.
1139                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                // reset scan range from `Scanning` to `Verify`
1249                state::set_scan_priority(
1250                    sync_state,
1251                    scan_range.block_range(),
1252                    ScanPriority::Verify,
1253                );
1254
1255                // extend verification range to VERIFY_BLOCK_RANGE_SIZE blocks below current verification range
1256                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
1292/// Processes mempool transaction.
1293///
1294/// Scan the transaction and add to the wallet if relevant.
1295async 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    // does not use raw transaction height due to lightwalletd off-by-one bug and potential to be zero
1305    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
1355/// Removes wallet blocks, transactions, nullifiers, outpoints and shard tree data above the given `truncate_height`.
1356fn 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/// Updates the wallet with data from `scan_results`
1446#[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    // add all block ranges of scan ranges with `ScannedWithoutMapping` or `RefetchingNullifiers` priority above the
1483    // current scan range to each note to track which ranges need the nullifiers to be re-fetched before the note is
1484    // known to be unspent (in addition to all other ranges above the notes height being `Scanned`,
1485    // `ScannedWithoutMapping` or `RefetchingNullifiers` priority). this information is necessary as these ranges have been scanned but the
1486    // nullifiers have been discarded so must be re-fetched. if ranges are scanned but the nullifiers are discarded
1487    // (set to `ScannedWithoutMapping` priority) *after* this note has been added to the wallet, this is sufficient to
1488    // know this note has not been spent, even if this range is not set to `Scanned` priority.
1489    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(|&note| note.key_id().scope == zip32::Scope::External)
1554    {
1555        let ivk = ufvks
1556            .get(&note.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(&note.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(|&note| note.key_id().scope == zip32::Scope::External)
1573    {
1574        let ivk = ufvks
1575            .get(&note.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(&note.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    // if the shard store only contains the first checkpoint added on initialisation, add frontiers to complete the
1774    // shard trees.
1775    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
1811/// Sets up mempool stream.
1812///
1813/// If there is some raw transaction, send to be scanned.
1814/// If the mempool stream message is `None` (a block was mined) or the request failed, setup a new mempool stream.
1815async 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
1866/// Spends will be reset to free up funds if transaction has been unconfirmed for
1867/// `UNCONFIRMED_SPEND_INVALIDATION_THRESHOLD` confirmed blocks.
1868/// Transaction status will then be set to `Failed` if it's still unconfirmed when the chain reaches it's expiry height.
1869// TODO: add config to pepper-sync to set UNCONFIRMED_SPEND_INVALIDATION_THRESHOLD
1870fn 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        // It's possible an error from an implementor's get_sync_state could bubble up to checked_wallet_height
1935        // this test shows that such an error is raies wrapped in a WalletError and return as the Err variant
1936        #[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                // The hain_height is received from the proxy
1997                // truncate uses the wallet scan start height
1998                // as a
1999                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                // This will trigger a call to truncate_wallet_data with
2011                // chain_height and start_height inferred from the wallet.
2012                // chain must be greater than by this time which hits the Greater cmp
2013                // match
2014                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                // This case requires that the wallet have a scan_start_below sapling
2035                // which is an unexpected state.
2036                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            // If there are know scan_ranges in the SyncState
2065            #[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}