Skip to main content

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_protocol::{
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::WalletWrite,
163    proto::compact_formats::CompactBlock,
164    scanning::{
165        ScanBlockError, ScanningKeys, SpendIdentifiers,
166        compact::{BatchRunners, scan_block_with_runners},
167    },
168};
169
170#[cfg(feature = "sync")]
171use {
172    super::scanning::ScanPriority, crate::data_api::scanning::ScanRange, async_trait::async_trait,
173};
174
175pub mod error;
176use error::Error;
177
178use super::WalletRead;
179
180/// A struct containing metadata about a subtree root of the note commitment tree.
181///
182/// This stores the block height at which the leaf that completed the subtree was
183/// added, and the root hash of the complete subtree.
184#[derive(Debug)]
185pub struct CommitmentTreeRoot<H> {
186    subtree_end_height: BlockHeight,
187    root_hash: H,
188}
189
190impl<H> CommitmentTreeRoot<H> {
191    /// Construct a new `CommitmentTreeRoot` from its constituent parts.
192    ///
193    /// - `subtree_end_height`: The height of the block containing the note commitment that
194    ///   completed the subtree.
195    /// - `root_hash`: The Merkle root of the completed subtree.
196    pub fn from_parts(subtree_end_height: BlockHeight, root_hash: H) -> Self {
197        Self {
198            subtree_end_height,
199            root_hash,
200        }
201    }
202
203    /// Returns the block height at which the leaf that completed the subtree was added.
204    pub fn subtree_end_height(&self) -> BlockHeight {
205        self.subtree_end_height
206    }
207
208    /// Returns the root of the complete subtree.
209    pub fn root_hash(&self) -> &H {
210        &self.root_hash
211    }
212}
213
214/// This trait provides sequential access to raw blockchain data via a callback-oriented
215/// API.
216pub trait BlockSource {
217    type Error;
218
219    /// Scan the specified `limit` number of blocks from the blockchain, starting at
220    /// `from_height`, applying the provided callback to each block. If `from_height`
221    /// is `None` then scanning will begin at the first available block.
222    ///
223    /// * `WalletErrT`: the types of errors produced by the wallet operations performed
224    ///   as part of processing each row.
225    /// * `NoteRefT`: the type of note identifiers in the wallet data store, for use in
226    ///   reporting errors related to specific notes.
227    fn with_blocks<F, WalletErrT>(
228        &self,
229        from_height: Option<BlockHeight>,
230        limit: Option<usize>,
231        with_block: F,
232    ) -> Result<(), error::Error<WalletErrT, Self::Error>>
233    where
234        F: FnMut(CompactBlock) -> Result<(), error::Error<WalletErrT, Self::Error>>;
235}
236
237/// `BlockCache` is a trait that extends `BlockSource` and defines methods for managing
238/// a cache of compact blocks.
239///
240/// # Examples
241///
242/// ```
243///    use async_trait::async_trait;
244///    use std::sync::{Arc, Mutex};
245///    use zcash_client_backend::data_api::{
246///        chain::{error, BlockCache, BlockSource},
247///        scanning::{ScanPriority, ScanRange},
248///    };
249///    use zcash_client_backend::proto::compact_formats::CompactBlock;
250///    use zcash_protocol::consensus::BlockHeight;
251///
252///    struct ExampleBlockCache {
253///        cached_blocks: Arc<Mutex<Vec<CompactBlock>>>,
254///    }
255///
256/// #    impl BlockSource for ExampleBlockCache {
257/// #        type Error = ();
258/// #
259/// #        fn with_blocks<F, WalletErrT>(
260/// #            &self,
261/// #            _from_height: Option<BlockHeight>,
262/// #            _limit: Option<usize>,
263/// #            _with_block: F,
264/// #        ) -> Result<(), error::Error<WalletErrT, Self::Error>>
265/// #        where
266/// #            F: FnMut(CompactBlock) -> Result<(), error::Error<WalletErrT, Self::Error>>,
267/// #        {
268/// #            Ok(())
269/// #        }
270/// #    }
271/// #
272///    #[async_trait]
273///    impl BlockCache for ExampleBlockCache {
274///        fn get_tip_height(&self, range: Option<&ScanRange>) -> Result<Option<BlockHeight>, Self::Error> {
275///            let cached_blocks = self.cached_blocks.lock().unwrap();
276///            let blocks: Vec<&CompactBlock> = match range {
277///                Some(range) => cached_blocks
278///                    .iter()
279///                    .filter(|&block| {
280///                        let block_height = BlockHeight::from_u32(block.height as u32);
281///                        range.block_range().contains(&block_height)
282///                    })
283///                    .collect(),
284///                None => cached_blocks.iter().collect(),
285///            };
286///            let highest_block = blocks.iter().max_by_key(|&&block| block.height);
287///            Ok(highest_block.map(|&block| BlockHeight::from_u32(block.height as u32)))
288///        }
289///
290///        async fn read(&self, range: &ScanRange) -> Result<Vec<CompactBlock>, Self::Error> {
291///            Ok(self
292///                .cached_blocks
293///                .lock()
294///                .unwrap()
295///                .iter()
296///                .filter(|block| {
297///                    let block_height = BlockHeight::from_u32(block.height as u32);
298///                    range.block_range().contains(&block_height)
299///                })
300///                .cloned()
301///                .collect())
302///        }
303///
304///        async fn insert(&self, mut compact_blocks: Vec<CompactBlock>) -> Result<(), Self::Error> {
305///            self.cached_blocks
306///                .lock()
307///                .unwrap()
308///                .append(&mut compact_blocks);
309///            Ok(())
310///        }
311///
312///        async fn delete(&self, range: ScanRange) -> Result<(), Self::Error> {
313///            self.cached_blocks
314///                .lock()
315///                .unwrap()
316///                .retain(|block| !range.block_range().contains(&BlockHeight::from_u32(block.height as u32)));
317///            Ok(())
318///        }
319///    }
320///
321///    // Example usage
322///    let rt = tokio::runtime::Runtime::new().unwrap();
323///    let mut block_cache = ExampleBlockCache {
324///        cached_blocks: Arc::new(Mutex::new(Vec::new())),
325///    };
326///    let range = ScanRange::from_parts(
327///        BlockHeight::from_u32(1)..BlockHeight::from_u32(3),
328///        ScanPriority::Historic,
329///    );
330/// #    let extsk = sapling::zip32::ExtendedSpendingKey::master(&[]).expect("the derivation path yields a valid key");
331/// #    let dfvk = extsk.to_diversifiable_full_viewing_key();
332/// #    let compact_block1 = zcash_client_backend::scanning::testing::fake_compact_block(
333/// #        1u32.into(),
334/// #        zcash_primitives::block::BlockHash([0; 32]),
335/// #        sapling::Nullifier([0; 32]),
336/// #        &dfvk,
337/// #        zcash_protocol::value::Zatoshis::const_from_u64(5),
338/// #        false,
339/// #        None,
340/// #    );
341/// #    let compact_block2 = zcash_client_backend::scanning::testing::fake_compact_block(
342/// #        2u32.into(),
343/// #        zcash_primitives::block::BlockHash([0; 32]),
344/// #        sapling::Nullifier([0; 32]),
345/// #        &dfvk,
346/// #        zcash_protocol::value::Zatoshis::const_from_u64(5),
347/// #        false,
348/// #        None,
349/// #    );
350///    let compact_blocks = vec![compact_block1, compact_block2];
351///
352///    // Insert blocks into the block cache
353///    rt.block_on(async {
354///        block_cache.insert(compact_blocks.clone()).await.unwrap();
355///    });
356///    assert_eq!(block_cache.cached_blocks.lock().unwrap().len(), 2);
357///
358///    // Find highest block in the block cache
359///    let get_tip_height = block_cache.get_tip_height(None).unwrap();
360///    assert_eq!(get_tip_height, Some(BlockHeight::from_u32(2)));
361///
362///    // Read from the block cache
363///    rt.block_on(async {
364///        let blocks_from_cache = block_cache.read(&range).await.unwrap();
365///        assert_eq!(blocks_from_cache, compact_blocks);
366///    });
367///
368///    // Truncate the block cache
369///    rt.block_on(async {
370///        block_cache.truncate(BlockHeight::from_u32(1)).await.unwrap();
371///    });
372///    assert_eq!(block_cache.cached_blocks.lock().unwrap().len(), 1);
373///    assert_eq!(
374///        block_cache.get_tip_height(None).unwrap(),
375///        Some(BlockHeight::from_u32(1))
376///    );
377///
378///    // Delete blocks from the block cache
379///    rt.block_on(async {
380///        block_cache.delete(range).await.unwrap();
381///    });
382///    assert_eq!(block_cache.cached_blocks.lock().unwrap().len(), 0);
383///    assert_eq!(block_cache.get_tip_height(None).unwrap(), None);
384/// ```
385#[cfg(feature = "sync")]
386#[async_trait]
387pub trait BlockCache: BlockSource + Send + Sync
388where
389    Self::Error: Send,
390{
391    /// Finds the height of the highest block known to the block cache within a specified range.
392    ///
393    /// If `range` is `None`, returns the tip of the entire cache.
394    /// If no blocks are found in the cache, returns Ok(`None`).
395    fn get_tip_height(&self, range: Option<&ScanRange>)
396    -> Result<Option<BlockHeight>, Self::Error>;
397
398    /// Retrieves contiguous compact blocks specified by the given `range` from the block cache.
399    ///
400    /// Short reads are allowed, meaning that this method may return fewer blocks than requested
401    /// provided that all returned blocks are contiguous and start from `range.block_range().start`.
402    ///
403    /// # Errors
404    ///
405    /// This method should return an error if contiguous blocks cannot be read from the cache,
406    /// indicating there are blocks missing.
407    async fn read(&self, range: &ScanRange) -> Result<Vec<CompactBlock>, Self::Error>;
408
409    /// Inserts a vec of compact blocks into the block cache.
410    ///
411    /// This method permits insertion of non-contiguous compact blocks.
412    async fn insert(&self, compact_blocks: Vec<CompactBlock>) -> Result<(), Self::Error>;
413
414    /// Removes all cached blocks above a specified block height.
415    async fn truncate(&self, block_height: BlockHeight) -> Result<(), Self::Error> {
416        if let Some(latest) = self.get_tip_height(None)? {
417            self.delete(ScanRange::from_parts(
418                Range {
419                    start: block_height + 1,
420                    end: latest + 1,
421                },
422                ScanPriority::Ignored,
423            ))
424            .await?;
425        }
426        Ok(())
427    }
428
429    /// Deletes a range of compact blocks from the block cache.
430    ///
431    /// # Errors
432    ///
433    /// In the case of an error, some blocks requested for deletion may remain in the block cache.
434    async fn delete(&self, range: ScanRange) -> Result<(), Self::Error>;
435}
436
437/// Metadata about modifications to the wallet state made in the course of scanning a set of
438/// blocks.
439#[derive(Clone, Debug)]
440pub struct ScanSummary {
441    pub(crate) scanned_range: Range<BlockHeight>,
442    pub(crate) spent_sapling_note_count: usize,
443    pub(crate) received_sapling_note_count: usize,
444    #[cfg(feature = "orchard")]
445    pub(crate) spent_orchard_note_count: usize,
446    #[cfg(feature = "orchard")]
447    pub(crate) received_orchard_note_count: usize,
448}
449
450impl ScanSummary {
451    /// Constructs a new [`ScanSummary`] for the provided block range.
452    pub(crate) fn for_range(scanned_range: Range<BlockHeight>) -> Self {
453        Self {
454            scanned_range,
455            spent_sapling_note_count: 0,
456            received_sapling_note_count: 0,
457            #[cfg(feature = "orchard")]
458            spent_orchard_note_count: 0,
459            #[cfg(feature = "orchard")]
460            received_orchard_note_count: 0,
461        }
462    }
463
464    /// Returns the range of blocks successfully scanned.
465    pub fn scanned_range(&self) -> Range<BlockHeight> {
466        self.scanned_range.clone()
467    }
468
469    /// Returns the number of our previously-detected Sapling notes that were spent in transactions
470    /// in blocks in the scanned range. If we have not yet detected a particular note as ours, for
471    /// example because we are scanning the chain in reverse height order, we will not detect it
472    /// being spent at this time.
473    pub fn spent_sapling_note_count(&self) -> usize {
474        self.spent_sapling_note_count
475    }
476
477    /// Returns the number of Sapling notes belonging to the wallet that were received in blocks in
478    /// the scanned range. Note that depending upon the scanning order, it is possible that some of
479    /// the received notes counted here may already have been spent in later blocks closer to the
480    /// chain tip.
481    pub fn received_sapling_note_count(&self) -> usize {
482        self.received_sapling_note_count
483    }
484
485    /// Returns the number of our previously-detected Orchard notes that were spent in transactions
486    /// in blocks in the scanned range. If we have not yet detected a particular note as ours, for
487    /// example because we are scanning the chain in reverse height order, we will not detect it
488    /// being spent at this time.
489    #[cfg(feature = "orchard")]
490    pub fn spent_orchard_note_count(&self) -> usize {
491        self.spent_orchard_note_count
492    }
493
494    /// Returns the number of Orchard notes belonging to the wallet that were received in blocks in
495    /// the scanned range. Note that depending upon the scanning order, it is possible that some of
496    /// the received notes counted here may already have been spent in later blocks closer to the
497    /// chain tip.
498    #[cfg(feature = "orchard")]
499    pub fn received_orchard_note_count(&self) -> usize {
500        self.received_orchard_note_count
501    }
502}
503
504/// The final note commitment tree state for each shielded pool, as of a particular block height.
505#[derive(Debug, Clone, PartialEq, Eq)]
506pub struct ChainState {
507    block_height: BlockHeight,
508    block_hash: BlockHash,
509    final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
510    #[cfg(feature = "orchard")]
511    final_orchard_tree:
512        Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>,
513    // The Ironwood note commitment tree is Orchard-shaped, but Ironwood is a distinct pool with
514    // its own tree, tracked separately from Orchard.
515    #[cfg(feature = "orchard")]
516    final_ironwood_tree:
517        Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>,
518}
519
520impl ChainState {
521    /// Construct a new empty chain state.
522    pub fn empty(block_height: BlockHeight, block_hash: BlockHash) -> Self {
523        Self {
524            block_height,
525            block_hash,
526            final_sapling_tree: Frontier::empty(),
527            #[cfg(feature = "orchard")]
528            final_orchard_tree: Frontier::empty(),
529            #[cfg(feature = "orchard")]
530            final_ironwood_tree: Frontier::empty(),
531        }
532    }
533
534    /// Construct a new [`ChainState`] from its constituent parts.
535    pub fn new(
536        block_height: BlockHeight,
537        block_hash: BlockHash,
538        final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
539        #[cfg(feature = "orchard")] final_orchard_tree: Frontier<
540            orchard::tree::MerkleHashOrchard,
541            { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
542        >,
543        #[cfg(feature = "orchard")] final_ironwood_tree: Frontier<
544            orchard::tree::MerkleHashOrchard,
545            { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
546        >,
547    ) -> Self {
548        Self {
549            block_height,
550            block_hash,
551            final_sapling_tree,
552            #[cfg(feature = "orchard")]
553            final_orchard_tree,
554            #[cfg(feature = "orchard")]
555            final_ironwood_tree,
556        }
557    }
558
559    /// Returns the block height to which this chain state applies.
560    pub fn block_height(&self) -> BlockHeight {
561        self.block_height
562    }
563
564    /// Return the hash of the block.
565    pub fn block_hash(&self) -> BlockHash {
566        self.block_hash
567    }
568
569    /// Returns the frontier of the Sapling note commitment tree as of the end of the block at
570    /// [`Self::block_height`].
571    pub fn final_sapling_tree(
572        &self,
573    ) -> &Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }> {
574        &self.final_sapling_tree
575    }
576
577    /// Returns the frontier of the Orchard note commitment tree as of the end of the block at
578    /// [`Self::block_height`].
579    #[cfg(feature = "orchard")]
580    pub fn final_orchard_tree(
581        &self,
582    ) -> &Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>
583    {
584        &self.final_orchard_tree
585    }
586
587    /// Returns the frontier of the Ironwood note commitment tree as of the end of the block at
588    /// [`Self::block_height`].
589    #[cfg(feature = "orchard")]
590    pub fn final_ironwood_tree(
591        &self,
592    ) -> &Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>
593    {
594        &self.final_ironwood_tree
595    }
596}
597
598/// Scans at most `limit` blocks from the provided block source in order to find transactions
599/// received by the accounts tracked in the provided wallet database.
600///
601/// This function will return after scanning at most `limit` new blocks, to enable the caller to
602/// update their UI with scanning progress.
603///
604/// ## Errors
605///
606/// - [`Error::BlockSource`] if the requested blocks cannot be read from `block_source`.
607/// - [`Error::Scan`] if a scanned block violates chain-continuity rules or contains note
608///   commitments that cannot be reconciled with the wallet's note commitment tree(s).
609/// - [`Error::Wallet`] if a wallet-database operation fails. This covers reading the tracked
610///   viewing keys, block metadata, and nullifiers, as well as persisting the scanned blocks via
611///   [`WalletWrite::put_blocks`]. In particular, a failure to update the note commitment trees
612///   with the scanned data is reported here (for example, the `zcash_client_sqlite` backend
613///   surfaces such a failure as a `PutBlocksCommitmentTree` error identifying the affected
614///   shielded pool and block range).
615///
616/// ## Panics
617///
618/// This method will panic if `from_height != from_state.block_height() + 1`.
619#[tracing::instrument(skip(params, block_source, data_db, from_state))]
620#[allow(clippy::type_complexity)]
621pub fn scan_cached_blocks<ParamsT, DbT, BlockSourceT>(
622    params: &ParamsT,
623    block_source: &BlockSourceT,
624    data_db: &mut DbT,
625    from_height: BlockHeight,
626    from_state: &ChainState,
627    limit: usize,
628) -> Result<ScanSummary, Error<<DbT as WalletRead>::Error, BlockSourceT::Error>>
629where
630    ParamsT: consensus::Parameters + Send + 'static,
631    BlockSourceT: BlockSource,
632    DbT: WalletWrite,
633    <DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + Sync + 'static,
634{
635    assert_eq!(from_height, from_state.block_height + 1);
636
637    // Fetch the UnifiedFullViewingKeys we are tracking
638    let account_ufvks = data_db
639        .get_unified_full_viewing_keys()
640        .map_err(Error::Wallet)?;
641    let scanning_keys = ScanningKeys::from_account_ufvks(account_ufvks);
642    let mut runners = BatchRunners::<_, (), (), ()>::for_keys(100, &scanning_keys);
643
644    block_source.with_blocks::<_, <DbT as WalletRead>::Error>(
645        Some(from_height),
646        Some(limit),
647        |block| runners.add_block(params, block).map_err(|e| e.into()),
648    )?;
649    runners.flush();
650
651    let mut prior_block_metadata = if from_height > BlockHeight::from(0) {
652        data_db
653            .block_metadata(from_height - 1)
654            .map_err(Error::Wallet)?
655    } else {
656        None
657    };
658
659    // Get the spend identifiers for the unspent outputs we are tracking.
660    let mut spend_ids = SpendIdentifiers::unspent(data_db).map_err(Error::Wallet)?;
661
662    // Nothing writes to the wallet until the whole batch has been scanned, so the wallet's set
663    // of transparent receivers is fixed for the duration of the scan and is resolved against a
664    // single snapshot rather than by a query per transparent output.
665    #[cfg(feature = "transparent-inputs")]
666    let transparent_receivers = data_db
667        .get_transparent_receiver_accounts()
668        .map_err(Error::Wallet)?;
669
670    let mut scanned_blocks = vec![];
671    let mut scan_summary = ScanSummary::for_range(from_height..from_height);
672    block_source.with_blocks::<_, <DbT as WalletRead>::Error>(
673        Some(from_height),
674        Some(limit),
675        |block: CompactBlock| {
676            scan_summary.scanned_range.end = block.height() + 1;
677            let scanned_block = scan_block_with_runners::<_, _, _, (), (), (), _>(
678                params,
679                block,
680                &scanning_keys,
681                &spend_ids,
682                prior_block_metadata.as_ref(),
683                Some(&mut runners),
684                #[cfg(feature = "transparent-inputs")]
685                |address| {
686                    Ok::<_, <DbT as WalletRead>::Error>(transparent_receivers.get(address).copied())
687                },
688            )
689            .map_err(|e| match e {
690                ScanBlockError::Scan(e) => Error::Scan(e),
691                ScanBlockError::AddressLookup(e) => Error::Wallet(e),
692            })?;
693
694            for wtx in &scanned_block.transactions {
695                scan_summary.spent_sapling_note_count += wtx.sapling_spends().len();
696                scan_summary.received_sapling_note_count += wtx.sapling_outputs().len();
697                #[cfg(feature = "orchard")]
698                {
699                    scan_summary.spent_orchard_note_count += wtx.orchard_spends().len();
700                    scan_summary.received_orchard_note_count += wtx.orchard_outputs().len();
701                }
702            }
703
704            spend_ids.update_with(&scanned_block);
705            prior_block_metadata = Some(scanned_block.to_block_metadata());
706            scanned_blocks.push(scanned_block);
707
708            Ok(())
709        },
710    )?;
711
712    data_db
713        .put_blocks(from_state, scanned_blocks)
714        .map_err(Error::Wallet)?;
715    Ok(scan_summary)
716}
717
718#[cfg(feature = "test-dependencies")]
719pub mod testing {
720    use std::convert::Infallible;
721    use zcash_protocol::consensus::BlockHeight;
722
723    use crate::proto::compact_formats::CompactBlock;
724
725    use super::{BlockSource, error::Error};
726
727    pub struct MockBlockSource;
728
729    impl BlockSource for MockBlockSource {
730        type Error = Infallible;
731
732        fn with_blocks<F, DbErrT>(
733            &self,
734            _from_height: Option<BlockHeight>,
735            _limit: Option<usize>,
736            _with_row: F,
737        ) -> Result<(), Error<DbErrT, Infallible>>
738        where
739            F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, Infallible>>,
740        {
741            Ok(())
742        }
743    }
744}