zcash_client_backend/data_api/
chain.rs

1#![allow(clippy::needless_doctest_main)]
2//! Tools for blockchain validation & scanning
3//!
4//! # Examples
5//!
6//! ```
7//! # #[cfg(feature = "test-dependencies")]
8//! # {
9//! use zcash_primitives::{
10//!     consensus::{BlockHeight, Network, Parameters},
11//! };
12//!
13//! use zcash_client_backend::{
14//!     data_api::{
15//!         WalletRead, WalletWrite, WalletCommitmentTrees,
16//!         chain::{
17//!             BlockSource,
18//!             CommitmentTreeRoot,
19//!             error::Error,
20//!             scan_cached_blocks,
21//!             testing as chain_testing,
22//!         },
23//!         scanning::ScanPriority,
24//!         testing,
25//!     },
26//! };
27//!
28//! # use std::convert::Infallible;
29//!
30//! # fn main() {
31//! #   test();
32//! # }
33//! #
34//! # fn test() -> Result<(), Error<(), Infallible>> {
35//! let network = Network::TestNetwork;
36//! let block_source = chain_testing::MockBlockSource;
37//! let mut wallet_db = testing::MockWalletDb::new(Network::TestNetwork);
38//!
39//! // 1) Download note commitment tree data from lightwalletd
40//! let roots: Vec<CommitmentTreeRoot<sapling::Node>> = unimplemented!();
41//!
42//! // 2) Pass the commitment tree data to the database.
43//! wallet_db.put_sapling_subtree_roots(0, &roots).unwrap();
44//!
45//! // 3) Download chain tip metadata from lightwalletd
46//! let tip_height: BlockHeight = unimplemented!();
47//!
48//! // 4) Notify the wallet of the updated chain tip.
49//! wallet_db.update_chain_tip(tip_height).map_err(Error::Wallet)?;
50//!
51//! // 5) Get the suggested scan ranges from the wallet database
52//! let mut scan_ranges = wallet_db.suggest_scan_ranges().map_err(Error::Wallet)?;
53//!
54//! // 6) Run the following loop until the wallet's view of the chain tip as of the previous wallet
55//! //    session is valid.
56//! loop {
57//!     // If there is a range of blocks that needs to be verified, it will always be returned as
58//!     // the first element of the vector of suggested ranges.
59//!     match scan_ranges.first() {
60//!         Some(scan_range) if scan_range.priority() == ScanPriority::Verify => {
61//!             // Download the chain state for the block prior to the start of the range you want
62//!             // to scan.
63//!             let chain_state = unimplemented!("get_chain_state(scan_range.block_range().start - 1)?;");
64//!             // Download the blocks in `scan_range` into the block source, overwriting any
65//!             // existing blocks in this range.
66//!             unimplemented!("cache_blocks(scan_range)?;");
67//!
68//!             // Scan the downloaded blocks
69//!             let scan_result = scan_cached_blocks(
70//!                 &network,
71//!                 &block_source,
72//!                 &mut wallet_db,
73//!                 scan_range.block_range().start,
74//!                 chain_state,
75//!                 scan_range.len()
76//!             );
77//!
78//!             // Check for scanning errors that indicate that the wallet's chain tip is out of
79//!             // sync with blockchain history.
80//!             match scan_result {
81//!                 Ok(_) => {
82//!                     // At this point, the cache and scanned data are locally consistent (though
83//!                     // not necessarily consistent with the latest chain tip - this would be
84//!                     // discovered the next time this codepath is executed after new blocks are
85//!                     // received) so we can break out of the loop.
86//!                     break;
87//!                 }
88//!                 Err(Error::Scan(err)) if err.is_continuity_error() => {
89//!                     // Pick a height to rewind to, which must be at least one block before
90//!                     // the height at which the error occurred, but may be an earlier height
91//!                     // determined based on heuristics such as the platform, available bandwidth,
92//!                     // size of recent CompactBlocks, etc.
93//!                     let rewind_height = err.at_height().saturating_sub(10);
94//!
95//!                     // Rewind to the chosen height.
96//!                     wallet_db.truncate_to_height(rewind_height).map_err(Error::Wallet)?;
97//!
98//!                     // Delete cached blocks from rewind_height onwards.
99//!                     //
100//!                     // This does imply that assumed-valid blocks will be re-downloaded, but it
101//!                     // is also possible that in the intervening time, a chain reorg has
102//!                     // occurred that orphaned some of those blocks.
103//!                     unimplemented!();
104//!                 }
105//!                 Err(other) => {
106//!                     // Handle or return other errors
107//!                 }
108//!             }
109//!
110//!             // In case we updated the suggested scan ranges, now re-request.
111//!             scan_ranges = wallet_db.suggest_scan_ranges().map_err(Error::Wallet)?;
112//!         }
113//!         _ => {
114//!             // Nothing to verify; break out of the loop
115//!             break;
116//!         }
117//!     }
118//! }
119//!
120//! // 7) Loop over the remaining suggested scan ranges, retrieving the requested data and calling
121//! //    `scan_cached_blocks` on each range. Periodically, or if a continuity error is
122//! //    encountered, this process should be repeated starting at step (3).
123//! let scan_ranges = wallet_db.suggest_scan_ranges().map_err(Error::Wallet)?;
124//! for scan_range in scan_ranges {
125//!     // Download the chain state for the block prior to the start of the range you want
126//!     // to scan.
127//!     let chain_state = unimplemented!("get_chain_state(scan_range.block_range().start - 1)?;");
128//!     // Download the blocks in `scan_range` into the block source. While in this example this
129//!     // step is performed in-line, it's fine for the download of scan ranges to be asynchronous
130//!     // and for the scanner to process the downloaded ranges as they become available in a
131//!     // separate thread. The scan ranges should also be broken down into smaller chunks as
132//!     // appropriate, and for ranges with priority `Historic` it can be useful to download and
133//!     // scan the range in reverse order (to discover more recent unspent notes sooner), or from
134//!     // the start and end of the range inwards.
135//!     unimplemented!("cache_blocks(scan_range)?;");
136//!
137//!     // Scan the downloaded blocks.
138//!     let scan_result = scan_cached_blocks(
139//!         &network,
140//!         &block_source,
141//!         &mut wallet_db,
142//!         scan_range.block_range().start,
143//!         chain_state,
144//!         scan_range.len()
145//!     )?;
146//!
147//!     // Handle scan errors, etc.
148//! }
149//! # Ok(())
150//! # }
151//! # }
152//! ```
153
154use std::ops::Range;
155
156use incrementalmerkletree::frontier::Frontier;
157use subtle::ConditionallySelectable;
158use zcash_primitives::block::BlockHash;
159use zcash_protocol::consensus::{self, BlockHeight};
160
161use crate::{
162    data_api::{NullifierQuery, WalletWrite},
163    proto::compact_formats::CompactBlock,
164    scanning::{scan_block_with_runners, BatchRunners, Nullifiers, ScanningKeys},
165};
166
167#[cfg(feature = "sync")]
168use {
169    super::scanning::ScanPriority, crate::data_api::scanning::ScanRange, async_trait::async_trait,
170};
171
172pub mod error;
173use error::Error;
174
175use super::WalletRead;
176
177/// A struct containing metadata about a subtree root of the note commitment tree.
178///
179/// This stores the block height at which the leaf that completed the subtree was
180/// added, and the root hash of the complete subtree.
181#[derive(Debug)]
182pub struct CommitmentTreeRoot<H> {
183    subtree_end_height: BlockHeight,
184    root_hash: H,
185}
186
187impl<H> CommitmentTreeRoot<H> {
188    /// Construct a new `CommitmentTreeRoot` from its constituent parts.
189    ///
190    /// - `subtree_end_height`: The height of the block containing the note commitment that
191    ///   completed the subtree.
192    /// - `root_hash`: The Merkle root of the completed subtree.
193    pub fn from_parts(subtree_end_height: BlockHeight, root_hash: H) -> Self {
194        Self {
195            subtree_end_height,
196            root_hash,
197        }
198    }
199
200    /// Returns the block height at which the leaf that completed the subtree was added.
201    pub fn subtree_end_height(&self) -> BlockHeight {
202        self.subtree_end_height
203    }
204
205    /// Returns the root of the complete subtree.
206    pub fn root_hash(&self) -> &H {
207        &self.root_hash
208    }
209}
210
211/// This trait provides sequential access to raw blockchain data via a callback-oriented
212/// API.
213pub trait BlockSource {
214    type Error;
215
216    /// Scan the specified `limit` number of blocks from the blockchain, starting at
217    /// `from_height`, applying the provided callback to each block. If `from_height`
218    /// is `None` then scanning will begin at the first available block.
219    ///
220    /// * `WalletErrT`: the types of errors produced by the wallet operations performed
221    ///   as part of processing each row.
222    /// * `NoteRefT`: the type of note identifiers in the wallet data store, for use in
223    ///   reporting errors related to specific notes.
224    fn with_blocks<F, WalletErrT>(
225        &self,
226        from_height: Option<BlockHeight>,
227        limit: Option<usize>,
228        with_block: F,
229    ) -> Result<(), error::Error<WalletErrT, Self::Error>>
230    where
231        F: FnMut(CompactBlock) -> Result<(), error::Error<WalletErrT, Self::Error>>;
232}
233
234/// `BlockCache` is a trait that extends `BlockSource` and defines methods for managing
235/// a cache of compact blocks.
236///
237/// # Examples
238///
239/// ```
240///    use async_trait::async_trait;
241///    use std::sync::{Arc, Mutex};
242///    use zcash_client_backend::data_api::{
243///        chain::{error, BlockCache, BlockSource},
244///        scanning::{ScanPriority, ScanRange},
245///    };
246///    use zcash_client_backend::proto::compact_formats::CompactBlock;
247///    use zcash_primitives::consensus::BlockHeight;
248///
249///    struct ExampleBlockCache {
250///        cached_blocks: Arc<Mutex<Vec<CompactBlock>>>,
251///    }
252///
253/// #    impl BlockSource for ExampleBlockCache {
254/// #        type Error = ();
255/// #
256/// #        fn with_blocks<F, WalletErrT>(
257/// #            &self,
258/// #            _from_height: Option<BlockHeight>,
259/// #            _limit: Option<usize>,
260/// #            _with_block: F,
261/// #        ) -> Result<(), error::Error<WalletErrT, Self::Error>>
262/// #        where
263/// #            F: FnMut(CompactBlock) -> Result<(), error::Error<WalletErrT, Self::Error>>,
264/// #        {
265/// #            Ok(())
266/// #        }
267/// #    }
268/// #
269///    #[async_trait]
270///    impl BlockCache for ExampleBlockCache {
271///        fn get_tip_height(&self, range: Option<&ScanRange>) -> Result<Option<BlockHeight>, Self::Error> {
272///            let cached_blocks = self.cached_blocks.lock().unwrap();
273///            let blocks: Vec<&CompactBlock> = match range {
274///                Some(range) => cached_blocks
275///                    .iter()
276///                    .filter(|&block| {
277///                        let block_height = BlockHeight::from_u32(block.height as u32);
278///                        range.block_range().contains(&block_height)
279///                    })
280///                    .collect(),
281///                None => cached_blocks.iter().collect(),
282///            };
283///            let highest_block = blocks.iter().max_by_key(|&&block| block.height);
284///            Ok(highest_block.map(|&block| BlockHeight::from_u32(block.height as u32)))
285///        }
286///
287///        async fn read(&self, range: &ScanRange) -> Result<Vec<CompactBlock>, Self::Error> {
288///            Ok(self
289///                .cached_blocks
290///                .lock()
291///                .unwrap()
292///                .iter()
293///                .filter(|block| {
294///                    let block_height = BlockHeight::from_u32(block.height as u32);
295///                    range.block_range().contains(&block_height)
296///                })
297///                .cloned()
298///                .collect())
299///        }
300///
301///        async fn insert(&self, mut compact_blocks: Vec<CompactBlock>) -> Result<(), Self::Error> {
302///            self.cached_blocks
303///                .lock()
304///                .unwrap()
305///                .append(&mut compact_blocks);
306///            Ok(())
307///        }
308///
309///        async fn delete(&self, range: ScanRange) -> Result<(), Self::Error> {
310///            self.cached_blocks
311///                .lock()
312///                .unwrap()
313///                .retain(|block| !range.block_range().contains(&BlockHeight::from_u32(block.height as u32)));
314///            Ok(())
315///        }
316///    }
317///
318///    // Example usage
319///    let rt = tokio::runtime::Runtime::new().unwrap();
320///    let mut block_cache = ExampleBlockCache {
321///        cached_blocks: Arc::new(Mutex::new(Vec::new())),
322///    };
323///    let range = ScanRange::from_parts(
324///        BlockHeight::from_u32(1)..BlockHeight::from_u32(3),
325///        ScanPriority::Historic,
326///    );
327/// #    let extsk = sapling::zip32::ExtendedSpendingKey::master(&[]);
328/// #    let dfvk = extsk.to_diversifiable_full_viewing_key();
329/// #    let compact_block1 = zcash_client_backend::scanning::testing::fake_compact_block(
330/// #        1u32.into(),
331/// #        zcash_primitives::block::BlockHash([0; 32]),
332/// #        sapling::Nullifier([0; 32]),
333/// #        &dfvk,
334/// #        zcash_primitives::transaction::components::amount::NonNegativeAmount::const_from_u64(5),
335/// #        false,
336/// #        None,
337/// #    );
338/// #    let compact_block2 = zcash_client_backend::scanning::testing::fake_compact_block(
339/// #        2u32.into(),
340/// #        zcash_primitives::block::BlockHash([0; 32]),
341/// #        sapling::Nullifier([0; 32]),
342/// #        &dfvk,
343/// #        zcash_primitives::transaction::components::amount::NonNegativeAmount::const_from_u64(5),
344/// #        false,
345/// #        None,
346/// #    );
347///    let compact_blocks = vec![compact_block1, compact_block2];
348///
349///    // Insert blocks into the block cache
350///    rt.block_on(async {
351///        block_cache.insert(compact_blocks.clone()).await.unwrap();
352///    });
353///    assert_eq!(block_cache.cached_blocks.lock().unwrap().len(), 2);
354///
355///    // Find highest block in the block cache
356///    let get_tip_height = block_cache.get_tip_height(None).unwrap();
357///    assert_eq!(get_tip_height, Some(BlockHeight::from_u32(2)));
358///
359///    // Read from the block cache
360///    rt.block_on(async {
361///        let blocks_from_cache = block_cache.read(&range).await.unwrap();
362///        assert_eq!(blocks_from_cache, compact_blocks);
363///    });
364///
365///    // Truncate the block cache
366///    rt.block_on(async {
367///        block_cache.truncate(BlockHeight::from_u32(1)).await.unwrap();
368///    });
369///    assert_eq!(block_cache.cached_blocks.lock().unwrap().len(), 1);
370///    assert_eq!(
371///        block_cache.get_tip_height(None).unwrap(),
372///        Some(BlockHeight::from_u32(1))
373///    );
374///
375///    // Delete blocks from the block cache
376///    rt.block_on(async {
377///        block_cache.delete(range).await.unwrap();
378///    });
379///    assert_eq!(block_cache.cached_blocks.lock().unwrap().len(), 0);
380///    assert_eq!(block_cache.get_tip_height(None).unwrap(), None);
381/// ```
382#[cfg(feature = "sync")]
383#[async_trait]
384pub trait BlockCache: BlockSource + Send + Sync
385where
386    Self::Error: Send,
387{
388    /// Finds the height of the highest block known to the block cache within a specified range.
389    ///
390    /// If `range` is `None`, returns the tip of the entire cache.
391    /// If no blocks are found in the cache, returns Ok(`None`).
392    fn get_tip_height(&self, range: Option<&ScanRange>)
393        -> Result<Option<BlockHeight>, Self::Error>;
394
395    /// Retrieves contiguous compact blocks specified by the given `range` from the block cache.
396    ///
397    /// Short reads are allowed, meaning that this method may return fewer blocks than requested
398    /// provided that all returned blocks are contiguous and start from `range.block_range().start`.
399    ///
400    /// # Errors
401    ///
402    /// This method should return an error if contiguous blocks cannot be read from the cache,
403    /// indicating there are blocks missing.
404    async fn read(&self, range: &ScanRange) -> Result<Vec<CompactBlock>, Self::Error>;
405
406    /// Inserts a vec of compact blocks into the block cache.
407    ///
408    /// This method permits insertion of non-contiguous compact blocks.
409    async fn insert(&self, compact_blocks: Vec<CompactBlock>) -> Result<(), Self::Error>;
410
411    /// Removes all cached blocks above a specified block height.
412    async fn truncate(&self, block_height: BlockHeight) -> Result<(), Self::Error> {
413        if let Some(latest) = self.get_tip_height(None)? {
414            self.delete(ScanRange::from_parts(
415                Range {
416                    start: block_height + 1,
417                    end: latest + 1,
418                },
419                ScanPriority::Ignored,
420            ))
421            .await?;
422        }
423        Ok(())
424    }
425
426    /// Deletes a range of compact blocks from the block cache.
427    ///
428    /// # Errors
429    ///
430    /// In the case of an error, some blocks requested for deletion may remain in the block cache.
431    async fn delete(&self, range: ScanRange) -> Result<(), Self::Error>;
432}
433
434/// Metadata about modifications to the wallet state made in the course of scanning a set of
435/// blocks.
436#[derive(Clone, Debug)]
437pub struct ScanSummary {
438    pub(crate) scanned_range: Range<BlockHeight>,
439    pub(crate) spent_sapling_note_count: usize,
440    pub(crate) received_sapling_note_count: usize,
441    #[cfg(feature = "orchard")]
442    pub(crate) spent_orchard_note_count: usize,
443    #[cfg(feature = "orchard")]
444    pub(crate) received_orchard_note_count: usize,
445}
446
447impl ScanSummary {
448    /// Constructs a new [`ScanSummary`] for the provided block range.
449    pub(crate) fn for_range(scanned_range: Range<BlockHeight>) -> Self {
450        Self {
451            scanned_range,
452            spent_sapling_note_count: 0,
453            received_sapling_note_count: 0,
454            #[cfg(feature = "orchard")]
455            spent_orchard_note_count: 0,
456            #[cfg(feature = "orchard")]
457            received_orchard_note_count: 0,
458        }
459    }
460
461    /// Returns the range of blocks successfully scanned.
462    pub fn scanned_range(&self) -> Range<BlockHeight> {
463        self.scanned_range.clone()
464    }
465
466    /// Returns the number of our previously-detected Sapling notes that were spent in transactions
467    /// in blocks in the scanned range. If we have not yet detected a particular note as ours, for
468    /// example because we are scanning the chain in reverse height order, we will not detect it
469    /// being spent at this time.
470    pub fn spent_sapling_note_count(&self) -> usize {
471        self.spent_sapling_note_count
472    }
473
474    /// Returns the number of Sapling notes belonging to the wallet that were received in blocks in
475    /// the scanned range. Note that depending upon the scanning order, it is possible that some of
476    /// the received notes counted here may already have been spent in later blocks closer to the
477    /// chain tip.
478    pub fn received_sapling_note_count(&self) -> usize {
479        self.received_sapling_note_count
480    }
481
482    /// Returns the number of our previously-detected Orchard notes that were spent in transactions
483    /// in blocks in the scanned range. If we have not yet detected a particular note as ours, for
484    /// example because we are scanning the chain in reverse height order, we will not detect it
485    /// being spent at this time.
486    #[cfg(feature = "orchard")]
487    pub fn spent_orchard_note_count(&self) -> usize {
488        self.spent_orchard_note_count
489    }
490
491    /// Returns the number of Orchard notes belonging to the wallet that were received in blocks in
492    /// the scanned range. Note that depending upon the scanning order, it is possible that some of
493    /// the received notes counted here may already have been spent in later blocks closer to the
494    /// chain tip.
495    #[cfg(feature = "orchard")]
496    pub fn received_orchard_note_count(&self) -> usize {
497        self.received_orchard_note_count
498    }
499}
500
501/// The final note commitment tree state for each shielded pool, as of a particular block height.
502#[derive(Debug, Clone)]
503pub struct ChainState {
504    block_height: BlockHeight,
505    block_hash: BlockHash,
506    final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
507    #[cfg(feature = "orchard")]
508    final_orchard_tree:
509        Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>,
510}
511
512impl ChainState {
513    /// Construct a new empty chain state.
514    pub fn empty(block_height: BlockHeight, block_hash: BlockHash) -> Self {
515        Self {
516            block_height,
517            block_hash,
518            final_sapling_tree: Frontier::empty(),
519            #[cfg(feature = "orchard")]
520            final_orchard_tree: Frontier::empty(),
521        }
522    }
523
524    /// Construct a new [`ChainState`] from its constituent parts.
525    pub fn new(
526        block_height: BlockHeight,
527        block_hash: BlockHash,
528        final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
529        #[cfg(feature = "orchard")] final_orchard_tree: Frontier<
530            orchard::tree::MerkleHashOrchard,
531            { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
532        >,
533    ) -> Self {
534        Self {
535            block_height,
536            block_hash,
537            final_sapling_tree,
538            #[cfg(feature = "orchard")]
539            final_orchard_tree,
540        }
541    }
542
543    /// Returns the block height to which this chain state applies.
544    pub fn block_height(&self) -> BlockHeight {
545        self.block_height
546    }
547
548    /// Return the hash of the block.
549    pub fn block_hash(&self) -> BlockHash {
550        self.block_hash
551    }
552
553    /// Returns the frontier of the Sapling note commitment tree as of the end of the block at
554    /// [`Self::block_height`].
555    pub fn final_sapling_tree(
556        &self,
557    ) -> &Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }> {
558        &self.final_sapling_tree
559    }
560
561    /// Returns the frontier of the Orchard note commitment tree as of the end of the block at
562    /// [`Self::block_height`].
563    #[cfg(feature = "orchard")]
564    pub fn final_orchard_tree(
565        &self,
566    ) -> &Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>
567    {
568        &self.final_orchard_tree
569    }
570}
571
572/// Scans at most `limit` blocks from the provided block source for in order to find transactions
573/// received by the accounts tracked in the provided wallet database.
574///
575/// This function will return after scanning at most `limit` new blocks, to enable the caller to
576/// update their UI with scanning progress. Repeatedly calling this function with `from_height ==
577/// None` will process sequential ranges of blocks.
578///
579/// ## Panics
580///
581/// This method will panic if `from_height != from_state.block_height() + 1`.
582#[tracing::instrument(skip(params, block_source, data_db, from_state))]
583#[allow(clippy::type_complexity)]
584pub fn scan_cached_blocks<ParamsT, DbT, BlockSourceT>(
585    params: &ParamsT,
586    block_source: &BlockSourceT,
587    data_db: &mut DbT,
588    from_height: BlockHeight,
589    from_state: &ChainState,
590    limit: usize,
591) -> Result<ScanSummary, Error<DbT::Error, BlockSourceT::Error>>
592where
593    ParamsT: consensus::Parameters + Send + 'static,
594    BlockSourceT: BlockSource,
595    DbT: WalletWrite,
596    <DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + 'static,
597{
598    assert_eq!(from_height, from_state.block_height + 1);
599
600    // Fetch the UnifiedFullViewingKeys we are tracking
601    let account_ufvks = data_db
602        .get_unified_full_viewing_keys()
603        .map_err(Error::Wallet)?;
604    let scanning_keys = ScanningKeys::from_account_ufvks(account_ufvks);
605    let mut runners = BatchRunners::<_, (), ()>::for_keys(100, &scanning_keys);
606
607    block_source.with_blocks::<_, DbT::Error>(Some(from_height), Some(limit), |block| {
608        runners.add_block(params, block).map_err(|e| e.into())
609    })?;
610    runners.flush();
611
612    let mut prior_block_metadata = if from_height > BlockHeight::from(0) {
613        data_db
614            .block_metadata(from_height - 1)
615            .map_err(Error::Wallet)?
616    } else {
617        None
618    };
619
620    // Get the nullifiers for the unspent notes we are tracking
621    let mut nullifiers = Nullifiers::new(
622        data_db
623            .get_sapling_nullifiers(NullifierQuery::Unspent)
624            .map_err(Error::Wallet)?,
625        #[cfg(feature = "orchard")]
626        data_db
627            .get_orchard_nullifiers(NullifierQuery::Unspent)
628            .map_err(Error::Wallet)?,
629    );
630
631    let mut scanned_blocks = vec![];
632    let mut scan_summary = ScanSummary::for_range(from_height..from_height);
633    block_source.with_blocks::<_, DbT::Error>(
634        Some(from_height),
635        Some(limit),
636        |block: CompactBlock| {
637            scan_summary.scanned_range.end = block.height() + 1;
638            let scanned_block = scan_block_with_runners::<_, _, _, (), ()>(
639                params,
640                block,
641                &scanning_keys,
642                &nullifiers,
643                prior_block_metadata.as_ref(),
644                Some(&mut runners),
645            )
646            .map_err(Error::Scan)?;
647
648            for wtx in &scanned_block.transactions {
649                scan_summary.spent_sapling_note_count += wtx.sapling_spends().len();
650                scan_summary.received_sapling_note_count += wtx.sapling_outputs().len();
651                #[cfg(feature = "orchard")]
652                {
653                    scan_summary.spent_orchard_note_count += wtx.orchard_spends().len();
654                    scan_summary.received_orchard_note_count += wtx.orchard_outputs().len();
655                }
656            }
657
658            let sapling_spent_nf: Vec<&sapling::Nullifier> = scanned_block
659                .transactions
660                .iter()
661                .flat_map(|tx| tx.sapling_spends().iter().map(|spend| spend.nf()))
662                .collect();
663            nullifiers.retain_sapling(|(_, nf)| !sapling_spent_nf.contains(&nf));
664            nullifiers.extend_sapling(scanned_block.transactions.iter().flat_map(|tx| {
665                tx.sapling_outputs()
666                    .iter()
667                    .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
668            }));
669
670            #[cfg(feature = "orchard")]
671            {
672                let orchard_spent_nf: Vec<&orchard::note::Nullifier> = scanned_block
673                    .transactions
674                    .iter()
675                    .flat_map(|tx| tx.orchard_spends().iter().map(|spend| spend.nf()))
676                    .collect();
677
678                nullifiers.retain_orchard(|(_, nf)| !orchard_spent_nf.contains(&nf));
679                nullifiers.extend_orchard(scanned_block.transactions.iter().flat_map(|tx| {
680                    tx.orchard_outputs()
681                        .iter()
682                        .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
683                }));
684            }
685
686            prior_block_metadata = Some(scanned_block.to_block_metadata());
687            scanned_blocks.push(scanned_block);
688
689            Ok(())
690        },
691    )?;
692
693    data_db
694        .put_blocks(from_state, scanned_blocks)
695        .map_err(Error::Wallet)?;
696    Ok(scan_summary)
697}
698
699#[cfg(feature = "test-dependencies")]
700pub mod testing {
701    use std::convert::Infallible;
702    use zcash_protocol::consensus::BlockHeight;
703
704    use crate::proto::compact_formats::CompactBlock;
705
706    use super::{error::Error, BlockSource};
707
708    pub struct MockBlockSource;
709
710    impl BlockSource for MockBlockSource {
711        type Error = Infallible;
712
713        fn with_blocks<F, DbErrT>(
714            &self,
715            _from_height: Option<BlockHeight>,
716            _limit: Option<usize>,
717            _with_row: F,
718        ) -> Result<(), Error<DbErrT, Infallible>>
719        where
720            F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, Infallible>>,
721        {
722            Ok(())
723        }
724    }
725}