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}