zcash_client_backend/
scanning.rs

1//! Tools for scanning a compact representation of the Zcash block chain.
2
3use std::collections::{HashMap, HashSet};
4use std::convert::TryFrom;
5use std::fmt::{self, Debug};
6use std::hash::Hash;
7
8use incrementalmerkletree::{Marking, Position, Retention};
9use sapling::{
10    note_encryption::{CompactOutputDescription, SaplingDomain},
11    SaplingIvk,
12};
13use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption};
14
15use tracing::{debug, trace};
16use zcash_keys::keys::UnifiedFullViewingKey;
17use zcash_note_encryption::{batch, BatchDomain, Domain, ShieldedOutput, COMPACT_NOTE_SIZE};
18use zcash_primitives::transaction::{components::sapling::zip212_enforcement, TxId};
19use zcash_protocol::{
20    consensus::{self, BlockHeight, NetworkUpgrade},
21    ShieldedProtocol,
22};
23use zip32::Scope;
24
25use crate::{
26    data_api::{BlockMetadata, ScannedBlock, ScannedBundles},
27    proto::compact_formats::CompactBlock,
28    scan::{Batch, BatchRunner, CompactDecryptor, DecryptedOutput, Tasks},
29    wallet::{WalletOutput, WalletSpend, WalletTx},
30};
31
32#[cfg(feature = "orchard")]
33use orchard::{
34    note_encryption::{CompactAction, OrchardDomain},
35    tree::MerkleHashOrchard,
36};
37
38#[cfg(not(feature = "orchard"))]
39use std::marker::PhantomData;
40
41/// A key that can be used to perform trial decryption and nullifier
42/// computation for a [`CompactSaplingOutput`] or [`CompactOrchardAction`].
43///
44/// The purpose of this trait is to enable [`scan_block`]
45/// and related methods to be used with either incoming viewing keys
46/// or full viewing keys, with the data returned from trial decryption
47/// being dependent upon the type of key used. In the case that an
48/// incoming viewing key is used, only the note and payment address
49/// will be returned; in the case of a full viewing key, the
50/// nullifier for the note can also be obtained.
51///
52/// [`CompactSaplingOutput`]: crate::proto::compact_formats::CompactSaplingOutput
53/// [`CompactOrchardAction`]: crate::proto::compact_formats::CompactOrchardAction
54/// [`scan_block`]: crate::scanning::scan_block
55pub trait ScanningKeyOps<D: Domain, AccountId, Nf> {
56    /// Prepare the key for use in batch trial decryption.
57    fn prepare(&self) -> D::IncomingViewingKey;
58
59    /// Returns the account identifier for this key. An account identifier corresponds
60    /// to at most a single unified spending key's worth of spend authority, such that
61    /// both received notes and change spendable by that spending authority will be
62    /// interpreted as belonging to that account.
63    fn account_id(&self) -> &AccountId;
64
65    /// Returns the [`zip32::Scope`] for which this key was derived, if known.
66    fn key_scope(&self) -> Option<Scope>;
67
68    /// Produces the nullifier for the specified note and witness, if possible.
69    ///
70    /// IVK-based implementations of this trait cannot successfully derive
71    /// nullifiers, in which this function will always return `None`.
72    fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf>;
73}
74
75impl<D: Domain, AccountId, Nf, K: ScanningKeyOps<D, AccountId, Nf>> ScanningKeyOps<D, AccountId, Nf>
76    for &K
77{
78    fn prepare(&self) -> D::IncomingViewingKey {
79        (*self).prepare()
80    }
81
82    fn account_id(&self) -> &AccountId {
83        (*self).account_id()
84    }
85
86    fn key_scope(&self) -> Option<Scope> {
87        (*self).key_scope()
88    }
89
90    fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
91        (*self).nf(note, note_position)
92    }
93}
94
95impl<D: Domain, AccountId, Nf> ScanningKeyOps<D, AccountId, Nf>
96    for Box<dyn ScanningKeyOps<D, AccountId, Nf>>
97{
98    fn prepare(&self) -> D::IncomingViewingKey {
99        self.as_ref().prepare()
100    }
101
102    fn account_id(&self) -> &AccountId {
103        self.as_ref().account_id()
104    }
105
106    fn key_scope(&self) -> Option<Scope> {
107        self.as_ref().key_scope()
108    }
109
110    fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
111        self.as_ref().nf(note, note_position)
112    }
113}
114
115/// An incoming viewing key, paired with an optional nullifier key and key source metadata.
116pub struct ScanningKey<Ivk, Nk, AccountId> {
117    ivk: Ivk,
118    nk: Option<Nk>,
119    account_id: AccountId,
120    key_scope: Option<Scope>,
121}
122
123impl<AccountId> ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>
124    for ScanningKey<sapling::SaplingIvk, sapling::NullifierDerivingKey, AccountId>
125{
126    fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey {
127        sapling::note_encryption::PreparedIncomingViewingKey::new(&self.ivk)
128    }
129
130    fn nf(&self, note: &sapling::Note, position: Position) -> Option<sapling::Nullifier> {
131        self.nk.as_ref().map(|key| note.nf(key, position.into()))
132    }
133
134    fn account_id(&self) -> &AccountId {
135        &self.account_id
136    }
137
138    fn key_scope(&self) -> Option<Scope> {
139        self.key_scope
140    }
141}
142
143impl<AccountId> ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>
144    for (AccountId, SaplingIvk)
145{
146    fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey {
147        sapling::note_encryption::PreparedIncomingViewingKey::new(&self.1)
148    }
149
150    fn nf(&self, _note: &sapling::Note, _position: Position) -> Option<sapling::Nullifier> {
151        None
152    }
153
154    fn account_id(&self) -> &AccountId {
155        &self.0
156    }
157
158    fn key_scope(&self) -> Option<Scope> {
159        None
160    }
161}
162
163#[cfg(feature = "orchard")]
164impl<AccountId> ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>
165    for ScanningKey<orchard::keys::IncomingViewingKey, orchard::keys::FullViewingKey, AccountId>
166{
167    fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey {
168        orchard::keys::PreparedIncomingViewingKey::new(&self.ivk)
169    }
170
171    fn nf(
172        &self,
173        note: &orchard::note::Note,
174        _position: Position,
175    ) -> Option<orchard::note::Nullifier> {
176        self.nk.as_ref().map(|key| note.nullifier(key))
177    }
178
179    fn account_id(&self) -> &AccountId {
180        &self.account_id
181    }
182
183    fn key_scope(&self) -> Option<Scope> {
184        self.key_scope
185    }
186}
187
188/// A set of keys to be used in scanning for decryptable transaction outputs.
189pub struct ScanningKeys<AccountId, IvkTag> {
190    sapling: HashMap<IvkTag, Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>>>,
191    #[cfg(feature = "orchard")]
192    orchard: HashMap<
193        IvkTag,
194        Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>>,
195    >,
196}
197
198impl<AccountId, IvkTag> ScanningKeys<AccountId, IvkTag> {
199    /// Constructs a new set of scanning keys.
200    pub fn new(
201        sapling: HashMap<
202            IvkTag,
203            Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>>,
204        >,
205        #[cfg(feature = "orchard")] orchard: HashMap<
206            IvkTag,
207            Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>>,
208        >,
209    ) -> Self {
210        Self {
211            sapling,
212            #[cfg(feature = "orchard")]
213            orchard,
214        }
215    }
216
217    /// Constructs a new empty set of scanning keys.
218    pub fn empty() -> Self {
219        Self {
220            sapling: HashMap::new(),
221            #[cfg(feature = "orchard")]
222            orchard: HashMap::new(),
223        }
224    }
225
226    /// Returns the Sapling keys to be used for incoming note detection.
227    pub fn sapling(
228        &self,
229    ) -> &HashMap<IvkTag, Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>>>
230    {
231        &self.sapling
232    }
233
234    /// Returns the Orchard keys to be used for incoming note detection.
235    #[cfg(feature = "orchard")]
236    pub fn orchard(
237        &self,
238    ) -> &HashMap<IvkTag, Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>>>
239    {
240        &self.orchard
241    }
242}
243
244impl<AccountId: Copy + Eq + Hash + 'static> ScanningKeys<AccountId, (AccountId, Scope)> {
245    /// Constructs a [`ScanningKeys`] from an iterator of [`UnifiedFullViewingKey`]s,
246    /// along with the account identifiers corresponding to those UFVKs.
247    pub fn from_account_ufvks(
248        ufvks: impl IntoIterator<Item = (AccountId, UnifiedFullViewingKey)>,
249    ) -> Self {
250        #![allow(clippy::type_complexity)]
251
252        let mut sapling: HashMap<
253            (AccountId, Scope),
254            Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>>,
255        > = HashMap::new();
256        #[cfg(feature = "orchard")]
257        let mut orchard: HashMap<
258            (AccountId, Scope),
259            Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>>,
260        > = HashMap::new();
261
262        for (account_id, ufvk) in ufvks {
263            if let Some(dfvk) = ufvk.sapling() {
264                for scope in [Scope::External, Scope::Internal] {
265                    sapling.insert(
266                        (account_id, scope),
267                        Box::new(ScanningKey {
268                            ivk: dfvk.to_ivk(scope),
269                            nk: Some(dfvk.to_nk(scope)),
270                            account_id,
271                            key_scope: Some(scope),
272                        }),
273                    );
274                }
275            }
276
277            #[cfg(feature = "orchard")]
278            if let Some(fvk) = ufvk.orchard() {
279                for scope in [Scope::External, Scope::Internal] {
280                    orchard.insert(
281                        (account_id, scope),
282                        Box::new(ScanningKey {
283                            ivk: fvk.to_ivk(scope),
284                            nk: Some(fvk.clone()),
285                            account_id,
286                            key_scope: Some(scope),
287                        }),
288                    );
289                }
290            }
291        }
292
293        Self {
294            sapling,
295            #[cfg(feature = "orchard")]
296            orchard,
297        }
298    }
299}
300
301/// The set of nullifiers being tracked by a wallet.
302pub struct Nullifiers<AccountId> {
303    sapling: Vec<(AccountId, sapling::Nullifier)>,
304    #[cfg(feature = "orchard")]
305    orchard: Vec<(AccountId, orchard::note::Nullifier)>,
306}
307
308impl<AccountId> Nullifiers<AccountId> {
309    /// Constructs a new empty set of nullifiers
310    pub fn empty() -> Self {
311        Self {
312            sapling: vec![],
313            #[cfg(feature = "orchard")]
314            orchard: vec![],
315        }
316    }
317
318    /// Construct a nullifier set from its constituent parts.
319    pub(crate) fn new(
320        sapling: Vec<(AccountId, sapling::Nullifier)>,
321        #[cfg(feature = "orchard")] orchard: Vec<(AccountId, orchard::note::Nullifier)>,
322    ) -> Self {
323        Self {
324            sapling,
325            #[cfg(feature = "orchard")]
326            orchard,
327        }
328    }
329
330    /// Returns the Sapling nullifiers for notes that the wallet is tracking.
331    pub fn sapling(&self) -> &[(AccountId, sapling::Nullifier)] {
332        self.sapling.as_ref()
333    }
334
335    /// Returns the Orchard nullifiers for notes that the wallet is tracking.
336    #[cfg(feature = "orchard")]
337    pub fn orchard(&self) -> &[(AccountId, orchard::note::Nullifier)] {
338        self.orchard.as_ref()
339    }
340
341    /// Discards Sapling nullifiers from the tracked nullifier set, retaining only those that
342    /// satisfy the given predicate.
343    pub(crate) fn retain_sapling(&mut self, f: impl Fn(&(AccountId, sapling::Nullifier)) -> bool) {
344        self.sapling.retain(f);
345    }
346
347    /// Adds the given nullifiers to the tracked nullifier set.
348    pub(crate) fn extend_sapling(
349        &mut self,
350        nfs: impl IntoIterator<Item = (AccountId, sapling::Nullifier)>,
351    ) {
352        self.sapling.extend(nfs);
353    }
354
355    #[cfg(feature = "orchard")]
356    pub(crate) fn retain_orchard(
357        &mut self,
358        f: impl Fn(&(AccountId, orchard::note::Nullifier)) -> bool,
359    ) {
360        self.orchard.retain(f);
361    }
362
363    #[cfg(feature = "orchard")]
364    pub(crate) fn extend_orchard(
365        &mut self,
366        nfs: impl IntoIterator<Item = (AccountId, orchard::note::Nullifier)>,
367    ) {
368        self.orchard.extend(nfs);
369    }
370}
371
372/// Errors that may occur in chain scanning
373#[derive(Clone, Debug)]
374pub enum ScanError {
375    /// The encoding of a compact Sapling output or compact Orchard action was invalid.
376    EncodingInvalid {
377        at_height: BlockHeight,
378        txid: TxId,
379        pool_type: ShieldedProtocol,
380        index: usize,
381    },
382
383    /// The hash of the parent block given by a proposed new chain tip does not match the hash of
384    /// the current chain tip.
385    PrevHashMismatch { at_height: BlockHeight },
386
387    /// The block height field of the proposed new block is not equal to the height of the previous
388    /// block + 1.
389    BlockHeightDiscontinuity {
390        prev_height: BlockHeight,
391        new_height: BlockHeight,
392    },
393
394    /// The note commitment tree size for the given protocol at the proposed new block is not equal
395    /// to the size at the previous block plus the count of this block's outputs.
396    TreeSizeMismatch {
397        protocol: ShieldedProtocol,
398        at_height: BlockHeight,
399        given: u32,
400        computed: u32,
401    },
402
403    /// The size of the note commitment tree for the given protocol was not provided as part of a
404    /// [`CompactBlock`] being scanned, making it impossible to construct the nullifier for a
405    /// detected note.
406    TreeSizeUnknown {
407        protocol: ShieldedProtocol,
408        at_height: BlockHeight,
409    },
410
411    /// We were provided chain metadata for a block containing note commitment tree metadata
412    /// that is invalidated by the data in the block itself. This may be caused by the presence
413    /// of default values in the chain metadata.
414    TreeSizeInvalid {
415        protocol: ShieldedProtocol,
416        at_height: BlockHeight,
417    },
418}
419
420impl ScanError {
421    /// Returns whether this error is the result of a failed continuity check
422    pub fn is_continuity_error(&self) -> bool {
423        use ScanError::*;
424        match self {
425            EncodingInvalid { .. } => false,
426            PrevHashMismatch { .. } => true,
427            BlockHeightDiscontinuity { .. } => true,
428            TreeSizeMismatch { .. } => true,
429            TreeSizeUnknown { .. } => false,
430            TreeSizeInvalid { .. } => false,
431        }
432    }
433
434    /// Returns the block height at which the scan error occurred
435    pub fn at_height(&self) -> BlockHeight {
436        use ScanError::*;
437        match self {
438            EncodingInvalid { at_height, .. } => *at_height,
439            PrevHashMismatch { at_height } => *at_height,
440            BlockHeightDiscontinuity { new_height, .. } => *new_height,
441            TreeSizeMismatch { at_height, .. } => *at_height,
442            TreeSizeUnknown { at_height, .. } => *at_height,
443            TreeSizeInvalid { at_height, .. } => *at_height,
444        }
445    }
446}
447
448impl fmt::Display for ScanError {
449    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
450        use ScanError::*;
451        match &self {
452            EncodingInvalid { txid, pool_type, index, .. } => write!(
453                f,
454                "{:?} output {} of transaction {} was improperly encoded.",
455                pool_type, index, txid
456            ),
457            PrevHashMismatch { at_height } => write!(
458                f,
459                "The parent hash of proposed block does not correspond to the block hash at height {}.",
460                at_height
461            ),
462            BlockHeightDiscontinuity { prev_height, new_height } => {
463                write!(f, "Block height discontinuity at height {}; previous height was: {}", new_height, prev_height)
464            }
465            TreeSizeMismatch { protocol, at_height, given, computed } => {
466                write!(f, "The {:?} note commitment tree size provided by a compact block did not match the expected size at height {}; given {}, expected {}", protocol, at_height, given, computed)
467            }
468            TreeSizeUnknown { protocol, at_height } => {
469                write!(f, "Unable to determine {:?} note commitment tree size at height {}", protocol, at_height)
470            }
471            TreeSizeInvalid { protocol, at_height } => {
472                write!(f, "Received invalid (potentially default) {:?} note commitment tree size metadata at height {}", protocol, at_height)
473            }
474        }
475    }
476}
477
478/// Scans a [`CompactBlock`] with a set of [`ScanningKeys`].
479///
480/// Returns a vector of [`WalletTx`]s decryptable by any of the given keys. If an output is
481/// decrypted by a full viewing key, the nullifiers of that output will also be computed.
482///
483/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
484/// [`WalletTx`]: crate::wallet::WalletTx
485pub fn scan_block<P, AccountId, IvkTag>(
486    params: &P,
487    block: CompactBlock,
488    scanning_keys: &ScanningKeys<AccountId, IvkTag>,
489    nullifiers: &Nullifiers<AccountId>,
490    prior_block_metadata: Option<&BlockMetadata>,
491) -> Result<ScannedBlock<AccountId>, ScanError>
492where
493    P: consensus::Parameters + Send + 'static,
494    AccountId: Default + Eq + Hash + ConditionallySelectable + Send + 'static,
495    IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
496{
497    scan_block_with_runners::<_, _, _, (), ()>(
498        params,
499        block,
500        scanning_keys,
501        nullifiers,
502        prior_block_metadata,
503        None,
504    )
505}
506
507type TaggedSaplingBatch<IvkTag> = Batch<
508    IvkTag,
509    SaplingDomain,
510    sapling::note_encryption::CompactOutputDescription,
511    CompactDecryptor,
512>;
513type TaggedSaplingBatchRunner<IvkTag, Tasks> = BatchRunner<
514    IvkTag,
515    SaplingDomain,
516    sapling::note_encryption::CompactOutputDescription,
517    CompactDecryptor,
518    Tasks,
519>;
520
521#[cfg(feature = "orchard")]
522type TaggedOrchardBatch<IvkTag> =
523    Batch<IvkTag, OrchardDomain, orchard::note_encryption::CompactAction, CompactDecryptor>;
524#[cfg(feature = "orchard")]
525type TaggedOrchardBatchRunner<IvkTag, Tasks> = BatchRunner<
526    IvkTag,
527    OrchardDomain,
528    orchard::note_encryption::CompactAction,
529    CompactDecryptor,
530    Tasks,
531>;
532
533pub(crate) trait SaplingTasks<IvkTag>: Tasks<TaggedSaplingBatch<IvkTag>> {}
534impl<IvkTag, T: Tasks<TaggedSaplingBatch<IvkTag>>> SaplingTasks<IvkTag> for T {}
535
536#[cfg(not(feature = "orchard"))]
537pub(crate) trait OrchardTasks<IvkTag> {}
538#[cfg(not(feature = "orchard"))]
539impl<IvkTag, T> OrchardTasks<IvkTag> for T {}
540
541#[cfg(feature = "orchard")]
542pub(crate) trait OrchardTasks<IvkTag>: Tasks<TaggedOrchardBatch<IvkTag>> {}
543#[cfg(feature = "orchard")]
544impl<IvkTag, T: Tasks<TaggedOrchardBatch<IvkTag>>> OrchardTasks<IvkTag> for T {}
545
546pub(crate) struct BatchRunners<IvkTag, TS: SaplingTasks<IvkTag>, TO: OrchardTasks<IvkTag>> {
547    sapling: TaggedSaplingBatchRunner<IvkTag, TS>,
548    #[cfg(feature = "orchard")]
549    orchard: TaggedOrchardBatchRunner<IvkTag, TO>,
550    #[cfg(not(feature = "orchard"))]
551    orchard: PhantomData<TO>,
552}
553
554impl<IvkTag, TS, TO> BatchRunners<IvkTag, TS, TO>
555where
556    IvkTag: Clone + Send + 'static,
557    TS: SaplingTasks<IvkTag>,
558    TO: OrchardTasks<IvkTag>,
559{
560    pub(crate) fn for_keys<AccountId>(
561        batch_size_threshold: usize,
562        scanning_keys: &ScanningKeys<AccountId, IvkTag>,
563    ) -> Self {
564        BatchRunners {
565            sapling: BatchRunner::new(
566                batch_size_threshold,
567                scanning_keys
568                    .sapling()
569                    .iter()
570                    .map(|(id, key)| (id.clone(), key.prepare())),
571            ),
572            #[cfg(feature = "orchard")]
573            orchard: BatchRunner::new(
574                batch_size_threshold,
575                scanning_keys
576                    .orchard()
577                    .iter()
578                    .map(|(id, key)| (id.clone(), key.prepare())),
579            ),
580            #[cfg(not(feature = "orchard"))]
581            orchard: PhantomData,
582        }
583    }
584
585    pub(crate) fn flush(&mut self) {
586        self.sapling.flush();
587        #[cfg(feature = "orchard")]
588        self.orchard.flush();
589    }
590
591    #[tracing::instrument(skip_all, fields(height = block.height))]
592    pub(crate) fn add_block<P>(&mut self, params: &P, block: CompactBlock) -> Result<(), ScanError>
593    where
594        P: consensus::Parameters + Send + 'static,
595        IvkTag: Copy + Send + 'static,
596    {
597        let block_hash = block.hash();
598        let block_height = block.height();
599        let zip212_enforcement = zip212_enforcement(params, block_height);
600
601        for tx in block.vtx.into_iter() {
602            let txid = tx.txid();
603
604            self.sapling.add_outputs(
605                block_hash,
606                txid,
607                |_| SaplingDomain::new(zip212_enforcement),
608                &tx.outputs
609                    .iter()
610                    .enumerate()
611                    .map(|(i, output)| {
612                        CompactOutputDescription::try_from(output).map_err(|_| {
613                            ScanError::EncodingInvalid {
614                                at_height: block_height,
615                                txid,
616                                pool_type: ShieldedProtocol::Sapling,
617                                index: i,
618                            }
619                        })
620                    })
621                    .collect::<Result<Vec<_>, _>>()?,
622            );
623
624            #[cfg(feature = "orchard")]
625            self.orchard.add_outputs(
626                block_hash,
627                txid,
628                OrchardDomain::for_compact_action,
629                &tx.actions
630                    .iter()
631                    .enumerate()
632                    .map(|(i, action)| {
633                        CompactAction::try_from(action).map_err(|_| ScanError::EncodingInvalid {
634                            at_height: block_height,
635                            txid,
636                            pool_type: ShieldedProtocol::Orchard,
637                            index: i,
638                        })
639                    })
640                    .collect::<Result<Vec<_>, _>>()?,
641            );
642        }
643
644        Ok(())
645    }
646}
647
648#[tracing::instrument(skip_all, fields(height = block.height))]
649pub(crate) fn scan_block_with_runners<P, AccountId, IvkTag, TS, TO>(
650    params: &P,
651    block: CompactBlock,
652    scanning_keys: &ScanningKeys<AccountId, IvkTag>,
653    nullifiers: &Nullifiers<AccountId>,
654    prior_block_metadata: Option<&BlockMetadata>,
655    mut batch_runners: Option<&mut BatchRunners<IvkTag, TS, TO>>,
656) -> Result<ScannedBlock<AccountId>, ScanError>
657where
658    P: consensus::Parameters + Send + 'static,
659    AccountId: Default + Eq + Hash + ConditionallySelectable + Send + 'static,
660    IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
661    TS: SaplingTasks<IvkTag> + Sync,
662    TO: OrchardTasks<IvkTag> + Sync,
663{
664    fn check_hash_continuity(
665        block: &CompactBlock,
666        prior_block_metadata: Option<&BlockMetadata>,
667    ) -> Option<ScanError> {
668        if let Some(prev) = prior_block_metadata {
669            if block.height() != prev.block_height() + 1 {
670                debug!(
671                    "Block height discontinuity at {:?}, previous was {:?} ",
672                    block.height(),
673                    prev.block_height()
674                );
675                return Some(ScanError::BlockHeightDiscontinuity {
676                    prev_height: prev.block_height(),
677                    new_height: block.height(),
678                });
679            }
680
681            if block.prev_hash() != prev.block_hash() {
682                debug!("Block hash discontinuity at {:?}", block.height());
683                return Some(ScanError::PrevHashMismatch {
684                    at_height: block.height(),
685                });
686            }
687        }
688
689        None
690    }
691
692    if let Some(scan_error) = check_hash_continuity(&block, prior_block_metadata) {
693        return Err(scan_error);
694    }
695
696    trace!("Block continuity okay at {:?}", block.height());
697
698    let cur_height = block.height();
699    let cur_hash = block.hash();
700    let zip212_enforcement = zip212_enforcement(params, cur_height);
701
702    let mut sapling_commitment_tree_size = prior_block_metadata
703        .and_then(|m| m.sapling_tree_size())
704        .map_or_else(
705            || {
706                block.chain_metadata.as_ref().map_or_else(
707                    || {
708                        // If we're below Sapling activation, or Sapling activation is not set, the tree size is zero
709                        params
710                            .activation_height(NetworkUpgrade::Sapling)
711                            .map_or_else(
712                                || Ok(0),
713                                |sapling_activation| {
714                                    if cur_height < sapling_activation {
715                                        Ok(0)
716                                    } else {
717                                        Err(ScanError::TreeSizeUnknown {
718                                            protocol: ShieldedProtocol::Sapling,
719                                            at_height: cur_height,
720                                        })
721                                    }
722                                },
723                            )
724                    },
725                    |m| {
726                        let sapling_output_count: u32 = block
727                            .vtx
728                            .iter()
729                            .map(|tx| tx.outputs.len())
730                            .sum::<usize>()
731                            .try_into()
732                            .expect("Sapling output count cannot exceed a u32");
733
734                        // The default for m.sapling_commitment_tree_size is zero, so we need to check
735                        // that the subtraction will not underflow; if it would do so, we were given
736                        // invalid chain metadata for a block with Sapling outputs.
737                        m.sapling_commitment_tree_size
738                            .checked_sub(sapling_output_count)
739                            .ok_or(ScanError::TreeSizeInvalid {
740                                protocol: ShieldedProtocol::Sapling,
741                                at_height: cur_height,
742                            })
743                    },
744                )
745            },
746            Ok,
747        )?;
748    let sapling_final_tree_size = sapling_commitment_tree_size
749        + block
750            .vtx
751            .iter()
752            .map(|tx| u32::try_from(tx.outputs.len()).unwrap())
753            .sum::<u32>();
754
755    #[cfg(feature = "orchard")]
756    let mut orchard_commitment_tree_size = prior_block_metadata
757        .and_then(|m| m.orchard_tree_size())
758        .map_or_else(
759            || {
760                block.chain_metadata.as_ref().map_or_else(
761                    || {
762                        // If we're below Orchard activation, or Orchard activation is not set, the tree size is zero
763                        params.activation_height(NetworkUpgrade::Nu5).map_or_else(
764                            || Ok(0),
765                            |orchard_activation| {
766                                if cur_height < orchard_activation {
767                                    Ok(0)
768                                } else {
769                                    Err(ScanError::TreeSizeUnknown {
770                                        protocol: ShieldedProtocol::Orchard,
771                                        at_height: cur_height,
772                                    })
773                                }
774                            },
775                        )
776                    },
777                    |m| {
778                        let orchard_action_count: u32 = block
779                            .vtx
780                            .iter()
781                            .map(|tx| tx.actions.len())
782                            .sum::<usize>()
783                            .try_into()
784                            .expect("Orchard action count cannot exceed a u32");
785
786                        // The default for m.orchard_commitment_tree_size is zero, so we need to check
787                        // that the subtraction will not underflow; if it would do so, we were given
788                        // invalid chain metadata for a block with Orchard actions.
789                        m.orchard_commitment_tree_size
790                            .checked_sub(orchard_action_count)
791                            .ok_or(ScanError::TreeSizeInvalid {
792                                protocol: ShieldedProtocol::Orchard,
793                                at_height: cur_height,
794                            })
795                    },
796                )
797            },
798            Ok,
799        )?;
800    #[cfg(feature = "orchard")]
801    let orchard_final_tree_size = orchard_commitment_tree_size
802        + block
803            .vtx
804            .iter()
805            .map(|tx| u32::try_from(tx.actions.len()).unwrap())
806            .sum::<u32>();
807
808    let mut wtxs: Vec<WalletTx<AccountId>> = vec![];
809    let mut sapling_nullifier_map = Vec::with_capacity(block.vtx.len());
810    let mut sapling_note_commitments: Vec<(sapling::Node, Retention<BlockHeight>)> = vec![];
811
812    #[cfg(feature = "orchard")]
813    let mut orchard_nullifier_map = Vec::with_capacity(block.vtx.len());
814    #[cfg(feature = "orchard")]
815    let mut orchard_note_commitments: Vec<(MerkleHashOrchard, Retention<BlockHeight>)> = vec![];
816
817    for tx in block.vtx.into_iter() {
818        let txid = tx.txid();
819        let tx_index =
820            u16::try_from(tx.index).expect("Cannot fit more than 2^16 transactions in a block");
821
822        let (sapling_spends, sapling_unlinked_nullifiers) = find_spent(
823            &tx.spends,
824            &nullifiers.sapling,
825            |spend| {
826                spend.nf().expect(
827                    "Could not deserialize nullifier for spend from protobuf representation.",
828                )
829            },
830            WalletSpend::from_parts,
831        );
832
833        sapling_nullifier_map.push((txid, tx_index, sapling_unlinked_nullifiers));
834
835        #[cfg(feature = "orchard")]
836        let orchard_spends = {
837            let (orchard_spends, orchard_unlinked_nullifiers) = find_spent(
838                &tx.actions,
839                &nullifiers.orchard,
840                |spend| {
841                    spend.nf().expect(
842                        "Could not deserialize nullifier for spend from protobuf representation.",
843                    )
844                },
845                WalletSpend::from_parts,
846            );
847            orchard_nullifier_map.push((txid, tx_index, orchard_unlinked_nullifiers));
848            orchard_spends
849        };
850
851        // Collect the set of accounts that were spent from in this transaction
852        let spent_from_accounts = sapling_spends.iter().map(|spend| spend.account_id());
853        #[cfg(feature = "orchard")]
854        let spent_from_accounts =
855            spent_from_accounts.chain(orchard_spends.iter().map(|spend| spend.account_id()));
856        let spent_from_accounts = spent_from_accounts.copied().collect::<HashSet<_>>();
857
858        let (sapling_outputs, mut sapling_nc) = find_received(
859            cur_height,
860            sapling_final_tree_size
861                == sapling_commitment_tree_size + u32::try_from(tx.outputs.len()).unwrap(),
862            txid,
863            sapling_commitment_tree_size,
864            &scanning_keys.sapling,
865            &spent_from_accounts,
866            &tx.outputs
867                .iter()
868                .enumerate()
869                .map(|(i, output)| {
870                    Ok((
871                        SaplingDomain::new(zip212_enforcement),
872                        CompactOutputDescription::try_from(output).map_err(|_| {
873                            ScanError::EncodingInvalid {
874                                at_height: cur_height,
875                                txid,
876                                pool_type: ShieldedProtocol::Sapling,
877                                index: i,
878                            }
879                        })?,
880                    ))
881                })
882                .collect::<Result<Vec<_>, _>>()?,
883            batch_runners
884                .as_mut()
885                .map(|runners| |txid| runners.sapling.collect_results(cur_hash, txid)),
886            |output| sapling::Node::from_cmu(&output.cmu),
887        );
888        sapling_note_commitments.append(&mut sapling_nc);
889        let has_sapling = !(sapling_spends.is_empty() && sapling_outputs.is_empty());
890
891        #[cfg(feature = "orchard")]
892        let (orchard_outputs, mut orchard_nc) = find_received(
893            cur_height,
894            orchard_final_tree_size
895                == orchard_commitment_tree_size + u32::try_from(tx.actions.len()).unwrap(),
896            txid,
897            orchard_commitment_tree_size,
898            &scanning_keys.orchard,
899            &spent_from_accounts,
900            &tx.actions
901                .iter()
902                .enumerate()
903                .map(|(i, action)| {
904                    let action = CompactAction::try_from(action).map_err(|_| {
905                        ScanError::EncodingInvalid {
906                            at_height: cur_height,
907                            txid,
908                            pool_type: ShieldedProtocol::Orchard,
909                            index: i,
910                        }
911                    })?;
912                    Ok((OrchardDomain::for_compact_action(&action), action))
913                })
914                .collect::<Result<Vec<_>, _>>()?,
915            batch_runners
916                .as_mut()
917                .map(|runners| |txid| runners.orchard.collect_results(cur_hash, txid)),
918            |output| MerkleHashOrchard::from_cmx(&output.cmx()),
919        );
920        #[cfg(feature = "orchard")]
921        orchard_note_commitments.append(&mut orchard_nc);
922
923        #[cfg(feature = "orchard")]
924        let has_orchard = !(orchard_spends.is_empty() && orchard_outputs.is_empty());
925        #[cfg(not(feature = "orchard"))]
926        let has_orchard = false;
927
928        if has_sapling || has_orchard {
929            wtxs.push(WalletTx::new(
930                txid,
931                tx_index as usize,
932                sapling_spends,
933                sapling_outputs,
934                #[cfg(feature = "orchard")]
935                orchard_spends,
936                #[cfg(feature = "orchard")]
937                orchard_outputs,
938            ));
939        }
940
941        sapling_commitment_tree_size +=
942            u32::try_from(tx.outputs.len()).expect("Sapling output count cannot exceed a u32");
943        #[cfg(feature = "orchard")]
944        {
945            orchard_commitment_tree_size +=
946                u32::try_from(tx.actions.len()).expect("Orchard action count cannot exceed a u32");
947        }
948    }
949
950    if let Some(chain_meta) = block.chain_metadata {
951        if chain_meta.sapling_commitment_tree_size != sapling_commitment_tree_size {
952            return Err(ScanError::TreeSizeMismatch {
953                protocol: ShieldedProtocol::Sapling,
954                at_height: cur_height,
955                given: chain_meta.sapling_commitment_tree_size,
956                computed: sapling_commitment_tree_size,
957            });
958        }
959
960        #[cfg(feature = "orchard")]
961        if chain_meta.orchard_commitment_tree_size != orchard_commitment_tree_size {
962            return Err(ScanError::TreeSizeMismatch {
963                protocol: ShieldedProtocol::Orchard,
964                at_height: cur_height,
965                given: chain_meta.orchard_commitment_tree_size,
966                computed: orchard_commitment_tree_size,
967            });
968        }
969    }
970
971    Ok(ScannedBlock::from_parts(
972        cur_height,
973        cur_hash,
974        block.time,
975        wtxs,
976        ScannedBundles::new(
977            sapling_commitment_tree_size,
978            sapling_note_commitments,
979            sapling_nullifier_map,
980        ),
981        #[cfg(feature = "orchard")]
982        ScannedBundles::new(
983            orchard_commitment_tree_size,
984            orchard_note_commitments,
985            orchard_nullifier_map,
986        ),
987    ))
988}
989
990/// Check for spent notes. The comparison against known-unspent nullifiers is done
991/// in constant time.
992fn find_spent<
993    AccountId: ConditionallySelectable + Default,
994    Spend,
995    Nf: ConstantTimeEq + Copy,
996    WS,
997>(
998    spends: &[Spend],
999    nullifiers: &[(AccountId, Nf)],
1000    extract_nf: impl Fn(&Spend) -> Nf,
1001    construct_wallet_spend: impl Fn(usize, Nf, AccountId) -> WS,
1002) -> (Vec<WS>, Vec<Nf>) {
1003    // TODO: this is O(|nullifiers| * |notes|); does using constant-time operations here really
1004    // make sense?
1005    let mut found_spent = vec![];
1006    let mut unlinked_nullifiers = Vec::with_capacity(spends.len());
1007    for (index, spend) in spends.iter().enumerate() {
1008        let spend_nf = extract_nf(spend);
1009
1010        // Find whether any tracked nullifier that matches this spend, and produce a
1011        // WalletShieldedSpend in constant time.
1012        let ct_spend = nullifiers
1013            .iter()
1014            .map(|&(account, nf)| CtOption::new(account, nf.ct_eq(&spend_nf)))
1015            .fold(
1016                CtOption::new(AccountId::default(), 0.into()),
1017                |first, next| CtOption::conditional_select(&next, &first, first.is_some()),
1018            )
1019            .map(|account| construct_wallet_spend(index, spend_nf, account));
1020
1021        if let Some(spend) = ct_spend.into() {
1022            found_spent.push(spend);
1023        } else {
1024            // This nullifier didn't match any we are currently tracking; save it in
1025            // case it matches an earlier block range we haven't scanned yet.
1026            unlinked_nullifiers.push(spend_nf);
1027        }
1028    }
1029
1030    (found_spent, unlinked_nullifiers)
1031}
1032
1033#[allow(clippy::too_many_arguments)]
1034#[allow(clippy::type_complexity)]
1035fn find_received<
1036    AccountId: Copy + Eq + Hash,
1037    D: BatchDomain,
1038    Nf,
1039    IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
1040    SK: ScanningKeyOps<D, AccountId, Nf>,
1041    Output: ShieldedOutput<D, COMPACT_NOTE_SIZE>,
1042    NoteCommitment,
1043>(
1044    block_height: BlockHeight,
1045    last_commitments_in_block: bool,
1046    txid: TxId,
1047    commitment_tree_size: u32,
1048    keys: &HashMap<IvkTag, SK>,
1049    spent_from_accounts: &HashSet<AccountId>,
1050    decoded: &[(D, Output)],
1051    batch_results: Option<
1052        impl FnOnce(TxId) -> HashMap<(TxId, usize), DecryptedOutput<IvkTag, D, ()>>,
1053    >,
1054    extract_note_commitment: impl Fn(&Output) -> NoteCommitment,
1055) -> (
1056    Vec<WalletOutput<D::Note, Nf, AccountId>>,
1057    Vec<(NoteCommitment, Retention<BlockHeight>)>,
1058) {
1059    // Check for incoming notes while incrementing tree and witnesses
1060    let (decrypted_opts, decrypted_len) = if let Some(collect_results) = batch_results {
1061        let mut decrypted = collect_results(txid);
1062        let decrypted_len = decrypted.len();
1063        (
1064            (0..decoded.len())
1065                .map(|i| {
1066                    decrypted
1067                        .remove(&(txid, i))
1068                        .map(|d_out| (d_out.ivk_tag, d_out.note))
1069                })
1070                .collect::<Vec<_>>(),
1071            decrypted_len,
1072        )
1073    } else {
1074        let mut ivks = Vec::with_capacity(keys.len());
1075        let mut ivk_lookup = Vec::with_capacity(keys.len());
1076        for (key_id, key) in keys.iter() {
1077            ivks.push(key.prepare());
1078            ivk_lookup.push(key_id);
1079        }
1080
1081        let mut decrypted_len = 0;
1082        (
1083            batch::try_compact_note_decryption(&ivks, decoded)
1084                .into_iter()
1085                .map(|v| {
1086                    v.map(|((note, _), ivk_idx)| {
1087                        decrypted_len += 1;
1088                        (*ivk_lookup[ivk_idx], note)
1089                    })
1090                })
1091                .collect::<Vec<_>>(),
1092            decrypted_len,
1093        )
1094    };
1095
1096    let mut shielded_outputs = Vec::with_capacity(decrypted_len);
1097    let mut note_commitments = Vec::with_capacity(decoded.len());
1098    for (output_idx, ((_, output), decrypted_note)) in
1099        decoded.iter().zip(decrypted_opts).enumerate()
1100    {
1101        // Collect block note commitments
1102        let node = extract_note_commitment(output);
1103        // If the commitment is the last in the block, ensure that is retained as a checkpoint
1104        let is_checkpoint = output_idx + 1 == decoded.len() && last_commitments_in_block;
1105        let retention = match (decrypted_note.is_some(), is_checkpoint) {
1106            (is_marked, true) => Retention::Checkpoint {
1107                id: block_height,
1108                marking: if is_marked {
1109                    Marking::Marked
1110                } else {
1111                    Marking::None
1112                },
1113            },
1114            (true, false) => Retention::Marked,
1115            (false, false) => Retention::Ephemeral,
1116        };
1117
1118        if let Some((key_id, note)) = decrypted_note {
1119            let key = keys
1120                .get(&key_id)
1121                .expect("Key is available for decrypted output");
1122
1123            // A note is marked as "change" if the account that received it
1124            // also spent notes in the same transaction. This will catch,
1125            // for instance:
1126            // - Change created by spending fractions of notes.
1127            // - Notes created by consolidation transactions.
1128            // - Notes sent from one account to itself.
1129            let is_change = spent_from_accounts.contains(key.account_id());
1130            let note_commitment_tree_position = Position::from(u64::from(
1131                commitment_tree_size + u32::try_from(output_idx).unwrap(),
1132            ));
1133            let nf = key.nf(&note, note_commitment_tree_position);
1134
1135            shielded_outputs.push(WalletOutput::from_parts(
1136                output_idx,
1137                output.ephemeral_key(),
1138                note,
1139                is_change,
1140                note_commitment_tree_position,
1141                nf,
1142                *key.account_id(),
1143                key.key_scope(),
1144            ));
1145        }
1146
1147        note_commitments.push((node, retention))
1148    }
1149
1150    (shielded_outputs, note_commitments)
1151}
1152
1153#[cfg(any(test, feature = "test-dependencies"))]
1154pub mod testing {
1155    use group::{
1156        ff::{Field, PrimeField},
1157        GroupEncoding,
1158    };
1159    use rand_core::{OsRng, RngCore};
1160    use sapling::{
1161        constants::SPENDING_KEY_GENERATOR,
1162        note_encryption::{sapling_note_encryption, SaplingDomain},
1163        util::generate_random_rseed,
1164        value::NoteValue,
1165        zip32::DiversifiableFullViewingKey,
1166        Nullifier,
1167    };
1168    use zcash_note_encryption::{Domain, COMPACT_NOTE_SIZE};
1169    use zcash_primitives::{
1170        block::BlockHash, transaction::components::sapling::zip212_enforcement,
1171    };
1172    use zcash_protocol::{
1173        consensus::{BlockHeight, Network},
1174        memo::MemoBytes,
1175        value::Zatoshis,
1176    };
1177
1178    use crate::proto::compact_formats::{
1179        self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
1180    };
1181
1182    fn random_compact_tx(mut rng: impl RngCore) -> CompactTx {
1183        let fake_nf = {
1184            let mut nf = vec![0; 32];
1185            rng.fill_bytes(&mut nf);
1186            nf
1187        };
1188        let fake_cmu = {
1189            let fake_cmu = bls12_381::Scalar::random(&mut rng);
1190            fake_cmu.to_repr().to_vec()
1191        };
1192        let fake_epk = {
1193            let mut buffer = [0; 64];
1194            rng.fill_bytes(&mut buffer);
1195            let fake_esk = jubjub::Fr::from_bytes_wide(&buffer);
1196            let fake_epk = SPENDING_KEY_GENERATOR * fake_esk;
1197            fake_epk.to_bytes().to_vec()
1198        };
1199        let cspend = CompactSaplingSpend { nf: fake_nf };
1200        let cout = CompactSaplingOutput {
1201            cmu: fake_cmu,
1202            ephemeral_key: fake_epk,
1203            ciphertext: vec![0; COMPACT_NOTE_SIZE],
1204        };
1205        let mut ctx = CompactTx::default();
1206        let mut txid = vec![0; 32];
1207        rng.fill_bytes(&mut txid);
1208        ctx.hash = txid;
1209        ctx.spends.push(cspend);
1210        ctx.outputs.push(cout);
1211        ctx
1212    }
1213
1214    /// Create a fake CompactBlock at the given height, with a transaction containing a
1215    /// single spend of the given nullifier and a single output paying the given address.
1216    /// Returns the CompactBlock.
1217    ///
1218    /// Set `initial_tree_sizes` to `None` to simulate a `CompactBlock` retrieved
1219    /// from a `lightwalletd` that is not currently tracking note commitment tree sizes.
1220    pub fn fake_compact_block(
1221        height: BlockHeight,
1222        prev_hash: BlockHash,
1223        nf: Nullifier,
1224        dfvk: &DiversifiableFullViewingKey,
1225        value: Zatoshis,
1226        tx_after: bool,
1227        initial_tree_sizes: Option<(u32, u32)>,
1228    ) -> CompactBlock {
1229        let zip212_enforcement = zip212_enforcement(&Network::TestNetwork, height);
1230        let to = dfvk.default_address().1;
1231
1232        // Create a fake Note for the account
1233        let mut rng = OsRng;
1234        let rseed = generate_random_rseed(zip212_enforcement, &mut rng);
1235        let note = sapling::Note::from_parts(to, NoteValue::from_raw(value.into()), rseed);
1236        let encryptor = sapling_note_encryption(
1237            Some(dfvk.fvk().ovk),
1238            note.clone(),
1239            MemoBytes::empty().into_bytes(),
1240            &mut rng,
1241        );
1242        let cmu = note.cmu().to_bytes().to_vec();
1243        let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec();
1244        let enc_ciphertext = encryptor.encrypt_note_plaintext();
1245
1246        // Create a fake CompactBlock containing the note
1247        let mut cb = CompactBlock {
1248            hash: {
1249                let mut hash = vec![0; 32];
1250                rng.fill_bytes(&mut hash);
1251                hash
1252            },
1253            prev_hash: prev_hash.0.to_vec(),
1254            height: height.into(),
1255            ..Default::default()
1256        };
1257
1258        // Add a random Sapling tx before ours
1259        {
1260            let mut tx = random_compact_tx(&mut rng);
1261            tx.index = cb.vtx.len() as u64;
1262            cb.vtx.push(tx);
1263        }
1264
1265        let cspend = CompactSaplingSpend { nf: nf.0.to_vec() };
1266        let cout = CompactSaplingOutput {
1267            cmu,
1268            ephemeral_key,
1269            ciphertext: enc_ciphertext[..52].to_vec(),
1270        };
1271        let mut ctx = CompactTx::default();
1272        let mut txid = vec![0; 32];
1273        rng.fill_bytes(&mut txid);
1274        ctx.hash = txid;
1275        ctx.spends.push(cspend);
1276        ctx.outputs.push(cout);
1277        ctx.index = cb.vtx.len() as u64;
1278        cb.vtx.push(ctx);
1279
1280        // Optionally add another random Sapling tx after ours
1281        if tx_after {
1282            let mut tx = random_compact_tx(&mut rng);
1283            tx.index = cb.vtx.len() as u64;
1284            cb.vtx.push(tx);
1285        }
1286
1287        cb.chain_metadata =
1288            initial_tree_sizes.map(|(initial_sapling_tree_size, initial_orchard_tree_size)| {
1289                compact::ChainMetadata {
1290                    sapling_commitment_tree_size: initial_sapling_tree_size
1291                        + cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::<u32>(),
1292                    orchard_commitment_tree_size: initial_orchard_tree_size
1293                        + cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::<u32>(),
1294                }
1295            });
1296
1297        cb
1298    }
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303
1304    use std::convert::Infallible;
1305
1306    use incrementalmerkletree::{Marking, Position, Retention};
1307    use sapling::Nullifier;
1308    use zcash_keys::keys::UnifiedSpendingKey;
1309    use zcash_primitives::block::BlockHash;
1310    use zcash_protocol::{
1311        consensus::{BlockHeight, Network},
1312        value::Zatoshis,
1313    };
1314    use zip32::AccountId;
1315
1316    use crate::{
1317        data_api::BlockMetadata,
1318        scanning::{BatchRunners, ScanningKeys},
1319    };
1320
1321    use super::{scan_block, scan_block_with_runners, testing::fake_compact_block, Nullifiers};
1322
1323    #[test]
1324    fn scan_block_with_my_tx() {
1325        fn go(scan_multithreaded: bool) {
1326            let network = Network::TestNetwork;
1327            let account = AccountId::ZERO;
1328            let usk =
1329                UnifiedSpendingKey::from_seed(&network, &[0u8; 32], account).expect("Valid USK");
1330            let ufvk = usk.to_unified_full_viewing_key();
1331            let sapling_dfvk = ufvk.sapling().expect("Sapling key is present").clone();
1332            let scanning_keys = ScanningKeys::from_account_ufvks([(account, ufvk)]);
1333
1334            let cb = fake_compact_block(
1335                1u32.into(),
1336                BlockHash([0; 32]),
1337                Nullifier([0; 32]),
1338                &sapling_dfvk,
1339                Zatoshis::const_from_u64(5),
1340                false,
1341                None,
1342            );
1343            assert_eq!(cb.vtx.len(), 2);
1344
1345            let mut batch_runners = if scan_multithreaded {
1346                let mut runners = BatchRunners::<_, (), ()>::for_keys(10, &scanning_keys);
1347                runners
1348                    .add_block(&Network::TestNetwork, cb.clone())
1349                    .unwrap();
1350                runners.flush();
1351
1352                Some(runners)
1353            } else {
1354                None
1355            };
1356
1357            let scanned_block = scan_block_with_runners(
1358                &network,
1359                cb,
1360                &scanning_keys,
1361                &Nullifiers::empty(),
1362                Some(&BlockMetadata::from_parts(
1363                    BlockHeight::from(0),
1364                    BlockHash([0u8; 32]),
1365                    Some(0),
1366                    #[cfg(feature = "orchard")]
1367                    Some(0),
1368                )),
1369                batch_runners.as_mut(),
1370            )
1371            .unwrap();
1372            let txs = scanned_block.transactions();
1373            assert_eq!(txs.len(), 1);
1374
1375            let tx = &txs[0];
1376            assert_eq!(tx.block_index(), 1);
1377            assert_eq!(tx.sapling_spends().len(), 0);
1378            assert_eq!(tx.sapling_outputs().len(), 1);
1379            assert_eq!(tx.sapling_outputs()[0].index(), 0);
1380            assert_eq!(tx.sapling_outputs()[0].account_id(), &account);
1381            assert_eq!(tx.sapling_outputs()[0].note().value().inner(), 5);
1382            assert_eq!(
1383                tx.sapling_outputs()[0].note_commitment_tree_position(),
1384                Position::from(1)
1385            );
1386
1387            assert_eq!(scanned_block.sapling().final_tree_size(), 2);
1388            assert_eq!(
1389                scanned_block
1390                    .sapling()
1391                    .commitments()
1392                    .iter()
1393                    .map(|(_, retention)| *retention)
1394                    .collect::<Vec<_>>(),
1395                vec![
1396                    Retention::Ephemeral,
1397                    Retention::Checkpoint {
1398                        id: scanned_block.height(),
1399                        marking: Marking::Marked
1400                    }
1401                ]
1402            );
1403        }
1404
1405        go(false);
1406        go(true);
1407    }
1408
1409    #[test]
1410    fn scan_block_with_txs_after_my_tx() {
1411        fn go(scan_multithreaded: bool) {
1412            let network = Network::TestNetwork;
1413            let account = AccountId::ZERO;
1414            let usk =
1415                UnifiedSpendingKey::from_seed(&network, &[0u8; 32], account).expect("Valid USK");
1416            let ufvk = usk.to_unified_full_viewing_key();
1417            let sapling_dfvk = ufvk.sapling().expect("Sapling key is present").clone();
1418            let scanning_keys = ScanningKeys::from_account_ufvks([(account, ufvk)]);
1419
1420            let cb = fake_compact_block(
1421                1u32.into(),
1422                BlockHash([0; 32]),
1423                Nullifier([0; 32]),
1424                &sapling_dfvk,
1425                Zatoshis::const_from_u64(5),
1426                true,
1427                Some((0, 0)),
1428            );
1429            assert_eq!(cb.vtx.len(), 3);
1430
1431            let mut batch_runners = if scan_multithreaded {
1432                let mut runners = BatchRunners::<_, (), ()>::for_keys(10, &scanning_keys);
1433                runners
1434                    .add_block(&Network::TestNetwork, cb.clone())
1435                    .unwrap();
1436                runners.flush();
1437
1438                Some(runners)
1439            } else {
1440                None
1441            };
1442
1443            let scanned_block = scan_block_with_runners(
1444                &network,
1445                cb,
1446                &scanning_keys,
1447                &Nullifiers::empty(),
1448                None,
1449                batch_runners.as_mut(),
1450            )
1451            .unwrap();
1452            let txs = scanned_block.transactions();
1453            assert_eq!(txs.len(), 1);
1454
1455            let tx = &txs[0];
1456            assert_eq!(tx.block_index(), 1);
1457            assert_eq!(tx.sapling_spends().len(), 0);
1458            assert_eq!(tx.sapling_outputs().len(), 1);
1459            assert_eq!(tx.sapling_outputs()[0].index(), 0);
1460            assert_eq!(tx.sapling_outputs()[0].account_id(), &AccountId::ZERO);
1461            assert_eq!(tx.sapling_outputs()[0].note().value().inner(), 5);
1462
1463            assert_eq!(
1464                scanned_block
1465                    .sapling()
1466                    .commitments()
1467                    .iter()
1468                    .map(|(_, retention)| *retention)
1469                    .collect::<Vec<_>>(),
1470                vec![
1471                    Retention::Ephemeral,
1472                    Retention::Marked,
1473                    Retention::Checkpoint {
1474                        id: scanned_block.height(),
1475                        marking: Marking::None
1476                    }
1477                ]
1478            );
1479        }
1480
1481        go(false);
1482        go(true);
1483    }
1484
1485    #[test]
1486    fn scan_block_with_my_spend() {
1487        let network = Network::TestNetwork;
1488        let account = AccountId::try_from(12).unwrap();
1489        let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32], account).expect("Valid USK");
1490        let ufvk = usk.to_unified_full_viewing_key();
1491        let scanning_keys = ScanningKeys::<AccountId, Infallible>::empty();
1492
1493        let nf = Nullifier([7; 32]);
1494        let nullifiers = Nullifiers::new(
1495            vec![(account, nf)],
1496            #[cfg(feature = "orchard")]
1497            vec![],
1498        );
1499
1500        let cb = fake_compact_block(
1501            1u32.into(),
1502            BlockHash([0; 32]),
1503            nf,
1504            ufvk.sapling().unwrap(),
1505            Zatoshis::const_from_u64(5),
1506            false,
1507            Some((0, 0)),
1508        );
1509        assert_eq!(cb.vtx.len(), 2);
1510
1511        let scanned_block = scan_block(&network, cb, &scanning_keys, &nullifiers, None).unwrap();
1512        let txs = scanned_block.transactions();
1513        assert_eq!(txs.len(), 1);
1514
1515        let tx = &txs[0];
1516        assert_eq!(tx.block_index(), 1);
1517        assert_eq!(tx.sapling_spends().len(), 1);
1518        assert_eq!(tx.sapling_outputs().len(), 0);
1519        assert_eq!(tx.sapling_spends()[0].index(), 0);
1520        assert_eq!(tx.sapling_spends()[0].nf(), &nf);
1521        assert_eq!(tx.sapling_spends()[0].account_id(), &account);
1522
1523        assert_eq!(
1524            scanned_block
1525                .sapling()
1526                .commitments()
1527                .iter()
1528                .map(|(_, retention)| *retention)
1529                .collect::<Vec<_>>(),
1530            vec![
1531                Retention::Ephemeral,
1532                Retention::Checkpoint {
1533                    id: scanned_block.height(),
1534                    marking: Marking::None
1535                }
1536            ]
1537        );
1538    }
1539}