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}