Skip to main content

zcash_client_backend/
scanning.rs

1//! Tools for scanning a compact representation of the Zcash block chain.
2
3// The `ScanError` accessors below match on its variants bare.
4use ScanError::*;
5
6use core::{
7    convert::TryFrom,
8    fmt::{self, Debug},
9};
10use std::{
11    collections::{HashMap, HashSet},
12    hash::Hash,
13};
14
15use incrementalmerkletree::{Marking, Position, Retention};
16use sapling::{SaplingIvk, note_encryption::SaplingDomain};
17use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption};
18
19use zcash_keys::keys::UnifiedFullViewingKey;
20use zcash_note_encryption::{BatchDomain, Domain, ShieldedOutput};
21use zcash_primitives::transaction::TxId;
22use zcash_protocol::{
23    ShieldedPool,
24    consensus::{self, BlockHeight},
25};
26use zip32::Scope;
27
28use crate::{
29    data_api::{BlockMetadata, NullifierQuery, ScannedBlock, WalletRead},
30    proto::compact_formats::CompactBlock,
31    scan::DecryptedOutput,
32    wallet::WalletOutput,
33};
34
35#[cfg(feature = "transparent-inputs")]
36use crate::wallet::WalletTransparentSpend;
37
38#[cfg(feature = "orchard")]
39use orchard::note_encryption::OrchardDomain;
40
41#[cfg(feature = "transparent-inputs")]
42use transparent::{address::TransparentAddress, bundle::OutPoint, keys::TransparentKeyScope};
43
44pub(crate) mod compact;
45pub mod full;
46
47/// A key that can be used to perform trial decryption and nullifier
48/// computation for a [`CompactSaplingOutput`] or [`CompactOrchardAction`].
49///
50/// The purpose of this trait is to enable [`scan_block`]
51/// and related methods to be used with either incoming viewing keys
52/// or full viewing keys, with the data returned from trial decryption
53/// being dependent upon the type of key used. In the case that an
54/// incoming viewing key is used, only the note and payment address
55/// will be returned; in the case of a full viewing key, the
56/// nullifier for the note can also be obtained.
57///
58/// [`CompactSaplingOutput`]: crate::proto::compact_formats::CompactSaplingOutput
59/// [`CompactOrchardAction`]: crate::proto::compact_formats::CompactOrchardAction
60/// [`scan_block`]: crate::scanning::scan_block
61pub trait ScanningKeyOps<D: Domain, AccountId, Nf> {
62    /// Prepare the key for use in batch trial decryption.
63    fn prepare(&self) -> D::IncomingViewingKey;
64
65    /// Returns the account identifier for this key. An account identifier corresponds
66    /// to at most a single unified spending key's worth of spend authority, such that
67    /// both received notes and change spendable by that spending authority will be
68    /// interpreted as belonging to that account.
69    fn account_id(&self) -> &AccountId;
70
71    /// Returns the [`zip32::Scope`] for which this key was derived, if known.
72    fn key_scope(&self) -> Option<Scope>;
73
74    /// Produces the nullifier for the specified note and witness, if possible.
75    ///
76    /// IVK-based implementations of this trait cannot successfully derive
77    /// nullifiers, in which this function will always return `None`.
78    fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf>;
79}
80
81impl<D: Domain, AccountId, Nf, K: ScanningKeyOps<D, AccountId, Nf>> ScanningKeyOps<D, AccountId, Nf>
82    for &K
83{
84    fn prepare(&self) -> D::IncomingViewingKey {
85        (*self).prepare()
86    }
87
88    fn account_id(&self) -> &AccountId {
89        (*self).account_id()
90    }
91
92    fn key_scope(&self) -> Option<Scope> {
93        (*self).key_scope()
94    }
95
96    fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
97        (*self).nf(note, note_position)
98    }
99}
100
101impl<D: Domain, AccountId, Nf> ScanningKeyOps<D, AccountId, Nf>
102    for Box<dyn ScanningKeyOps<D, AccountId, Nf>>
103{
104    fn prepare(&self) -> D::IncomingViewingKey {
105        self.as_ref().prepare()
106    }
107
108    fn account_id(&self) -> &AccountId {
109        self.as_ref().account_id()
110    }
111
112    fn key_scope(&self) -> Option<Scope> {
113        self.as_ref().key_scope()
114    }
115
116    fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
117        self.as_ref().nf(note, note_position)
118    }
119}
120
121impl<D: Domain, AccountId: Send + Sync, Nf> ScanningKeyOps<D, AccountId, Nf>
122    for Box<dyn ScanningKeyOps<D, AccountId, Nf> + Send + Sync>
123{
124    fn prepare(&self) -> D::IncomingViewingKey {
125        self.as_ref().prepare()
126    }
127
128    fn account_id(&self) -> &AccountId {
129        self.as_ref().account_id()
130    }
131
132    fn key_scope(&self) -> Option<Scope> {
133        self.as_ref().key_scope()
134    }
135
136    fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
137        self.as_ref().nf(note, note_position)
138    }
139}
140
141/// An incoming viewing key, paired with an optional nullifier key and key source metadata.
142pub struct ScanningKey<Ivk, Nk, AccountId> {
143    ivk: Ivk,
144    nk: Option<Nk>,
145    account_id: AccountId,
146    key_scope: Option<Scope>,
147}
148
149impl<Ivk, Nk, AccountId> ScanningKey<Ivk, Nk, AccountId> {
150    /// Constructs a new `ScanningKey` from its constituent parts.
151    pub fn new(ivk: Ivk, nk: Option<Nk>, account_id: AccountId, key_scope: Option<Scope>) -> Self {
152        Self {
153            ivk,
154            nk,
155            account_id,
156            key_scope,
157        }
158    }
159}
160
161impl<AccountId> ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>
162    for ScanningKey<sapling::SaplingIvk, sapling::NullifierDerivingKey, AccountId>
163{
164    fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey {
165        sapling::note_encryption::PreparedIncomingViewingKey::new(&self.ivk)
166    }
167
168    fn nf(&self, note: &sapling::Note, position: Position) -> Option<sapling::Nullifier> {
169        self.nk.as_ref().map(|key| note.nf(key, position.into()))
170    }
171
172    fn account_id(&self) -> &AccountId {
173        &self.account_id
174    }
175
176    fn key_scope(&self) -> Option<Scope> {
177        self.key_scope
178    }
179}
180
181impl<AccountId> ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>
182    for (AccountId, SaplingIvk)
183{
184    fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey {
185        sapling::note_encryption::PreparedIncomingViewingKey::new(&self.1)
186    }
187
188    fn nf(&self, _note: &sapling::Note, _position: Position) -> Option<sapling::Nullifier> {
189        None
190    }
191
192    fn account_id(&self) -> &AccountId {
193        &self.0
194    }
195
196    fn key_scope(&self) -> Option<Scope> {
197        None
198    }
199}
200
201#[cfg(feature = "orchard")]
202impl<AccountId> ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>
203    for ScanningKey<orchard::keys::IncomingViewingKey, orchard::keys::FullViewingKey, AccountId>
204{
205    fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey {
206        orchard::keys::PreparedIncomingViewingKey::new(&self.ivk)
207    }
208
209    fn nf(
210        &self,
211        note: &orchard::note::Note,
212        _position: Position,
213    ) -> Option<orchard::note::Nullifier> {
214        self.nk.as_ref().map(|key| note.nullifier(key))
215    }
216
217    fn account_id(&self) -> &AccountId {
218        &self.account_id
219    }
220
221    fn key_scope(&self) -> Option<Scope> {
222        self.key_scope
223    }
224}
225
226/// The note-encryption domain used to trial-decrypt Ironwood outputs.
227///
228/// Ironwood ([ZIP 2005], NU6.3) is a distinct shielded pool. Its notes are Orchard-shaped and use
229/// the Orchard commitment and nullifier types, and are decrypted with the same Orchard viewing
230/// keys, but they carry version 3 note plaintexts and so require the Ironwood note-encryption
231/// domain, which is distinct from [`OrchardDomain`] (that domain accepts only version 2
232/// plaintexts). This is why Ironwood is a separate pool rather than a variant of Orchard.
233///
234/// [ZIP 2005]: https://zips.z.cash/zip-2005
235#[cfg(feature = "orchard")]
236pub(crate) type IronwoodDomain = orchard::note_encryption::IronwoodDomain;
237
238/// The nullifier type for Ironwood notes. This is the Orchard nullifier type, which does not
239/// depend on the note plaintext version. See [`IronwoodDomain`].
240#[cfg(feature = "orchard")]
241pub(crate) type IronwoodNullifier = orchard::note::Nullifier;
242
243/// An Orchard viewing key trial-decrypts Ironwood outputs under the Ironwood note-encryption
244/// domain. The key material is the same as for Orchard; only the domain (and thus the accepted
245/// note plaintext version) differs.
246#[cfg(feature = "orchard")]
247impl<AccountId> ScanningKeyOps<IronwoodDomain, AccountId, orchard::note::Nullifier>
248    for ScanningKey<orchard::keys::IncomingViewingKey, orchard::keys::FullViewingKey, AccountId>
249{
250    fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey {
251        orchard::keys::PreparedIncomingViewingKey::new(&self.ivk)
252    }
253
254    fn nf(
255        &self,
256        note: &orchard::note::Note,
257        _position: Position,
258    ) -> Option<orchard::note::Nullifier> {
259        self.nk.as_ref().map(|key| note.nullifier(key))
260    }
261
262    fn account_id(&self) -> &AccountId {
263        &self.account_id
264    }
265
266    fn key_scope(&self) -> Option<Scope> {
267        self.key_scope
268    }
269}
270
271/// A set of keys to be used in scanning for decryptable transaction outputs.
272pub struct ScanningKeys<AccountId, IvkTag> {
273    sapling: HashMap<
274        IvkTag,
275        Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
276    >,
277    #[cfg(feature = "orchard")]
278    orchard: HashMap<
279        IvkTag,
280        Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier> + Send + Sync>,
281    >,
282    #[cfg(feature = "orchard")]
283    ironwood: HashMap<
284        IvkTag,
285        Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
286    >,
287}
288
289impl<AccountId, IvkTag> ScanningKeys<AccountId, IvkTag> {
290    /// Constructs a new set of scanning keys.
291    pub fn new(
292        sapling: HashMap<
293            IvkTag,
294            Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
295        >,
296        #[cfg(feature = "orchard")] orchard: HashMap<
297            IvkTag,
298            Box<
299                dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>
300                    + Send
301                    + Sync,
302            >,
303        >,
304        #[cfg(feature = "orchard")] ironwood: HashMap<
305            IvkTag,
306            Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
307        >,
308    ) -> Self {
309        Self {
310            sapling,
311            #[cfg(feature = "orchard")]
312            orchard,
313            #[cfg(feature = "orchard")]
314            ironwood,
315        }
316    }
317
318    /// Constructs a new empty set of scanning keys.
319    pub fn empty() -> Self {
320        Self {
321            sapling: HashMap::new(),
322            #[cfg(feature = "orchard")]
323            orchard: HashMap::new(),
324            #[cfg(feature = "orchard")]
325            ironwood: HashMap::new(),
326        }
327    }
328
329    /// Returns the Sapling keys to be used for incoming note detection.
330    pub fn sapling(
331        &self,
332    ) -> &HashMap<
333        IvkTag,
334        Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
335    > {
336        &self.sapling
337    }
338
339    /// Returns the Orchard keys to be used for incoming note detection.
340    #[cfg(feature = "orchard")]
341    pub fn orchard(
342        &self,
343    ) -> &HashMap<
344        IvkTag,
345        Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier> + Send + Sync>,
346    > {
347        &self.orchard
348    }
349
350    /// Returns the Ironwood keys to be used for incoming note detection.
351    ///
352    /// Ironwood outputs are trial-decrypted with the account's Orchard viewing keys (see
353    /// [`orchard::note_encryption::IronwoodDomain`]), but are tracked as a separate pool from
354    /// Orchard.
355    #[cfg(feature = "orchard")]
356    pub fn ironwood(
357        &self,
358    ) -> &HashMap<
359        IvkTag,
360        Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
361    > {
362        &self.ironwood
363    }
364}
365
366impl<AccountId: Copy + Eq + Hash + Send + Sync + 'static>
367    ScanningKeys<AccountId, (AccountId, Scope)>
368{
369    /// Constructs a [`ScanningKeys`] from an iterator of [`UnifiedFullViewingKey`]s,
370    /// along with the account identifiers corresponding to those UFVKs.
371    pub fn from_account_ufvks(
372        ufvks: impl IntoIterator<Item = (AccountId, UnifiedFullViewingKey)>,
373    ) -> Self {
374        #![allow(clippy::type_complexity)]
375
376        let mut sapling: HashMap<
377            (AccountId, Scope),
378            Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
379        > = HashMap::new();
380        #[cfg(feature = "orchard")]
381        let mut orchard: HashMap<
382            (AccountId, Scope),
383            Box<
384                dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>
385                    + Send
386                    + Sync,
387            >,
388        > = HashMap::new();
389        #[cfg(feature = "orchard")]
390        let mut ironwood: HashMap<
391            (AccountId, Scope),
392            Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
393        > = HashMap::new();
394
395        for (account_id, ufvk) in ufvks {
396            if let Some(dfvk) = ufvk.sapling() {
397                for scope in [Scope::External, Scope::Internal] {
398                    sapling.insert(
399                        (account_id, scope),
400                        Box::new(ScanningKey {
401                            ivk: dfvk.to_ivk(scope),
402                            nk: Some(dfvk.to_nk(scope)),
403                            account_id,
404                            key_scope: Some(scope),
405                        }),
406                    );
407                }
408            }
409
410            #[cfg(feature = "orchard")]
411            if let Some(fvk) = ufvk.orchard() {
412                for scope in [Scope::External, Scope::Internal] {
413                    orchard.insert(
414                        (account_id, scope),
415                        Box::new(ScanningKey {
416                            ivk: fvk.to_ivk(scope),
417                            nk: Some(fvk.clone()),
418                            account_id,
419                            key_scope: Some(scope),
420                        }),
421                    );
422                }
423
424                // Ironwood outputs are decrypted with the same Orchard viewing keys, but are
425                // tracked as a separate pool.
426                for scope in [Scope::External, Scope::Internal] {
427                    ironwood.insert(
428                        (account_id, scope),
429                        Box::new(ScanningKey {
430                            ivk: fvk.to_ivk(scope),
431                            nk: Some(fvk.clone()),
432                            account_id,
433                            key_scope: Some(scope),
434                        }),
435                    );
436                }
437            }
438        }
439
440        Self {
441            sapling,
442            #[cfg(feature = "orchard")]
443            orchard,
444            #[cfg(feature = "orchard")]
445            ironwood,
446        }
447    }
448}
449
450/// The nullifiers of the wallet's unspent shielded notes, whose appearance in a scanned block
451/// reveals a spend of wallet funds.
452///
453/// Nullifiers are held in flat vectors because `find_spent` compares them in constant time, so
454/// that a local side-channel observer does not learn which of a block's nullifiers are the
455/// wallet's. That mitigation is partial: the comparison runs once per tracked nullifier, so its
456/// duration still discloses how many notes the wallet holds.
457pub struct Nullifiers<AccountId> {
458    sapling: Vec<(AccountId, sapling::Nullifier)>,
459    #[cfg(feature = "orchard")]
460    orchard: Vec<(AccountId, orchard::note::Nullifier)>,
461    #[cfg(feature = "orchard")]
462    ironwood: Vec<(AccountId, IronwoodNullifier)>,
463}
464
465impl<AccountId> Nullifiers<AccountId> {
466    /// Constructs a new empty set of nullifiers.
467    pub fn empty() -> Self {
468        Self {
469            sapling: vec![],
470            #[cfg(feature = "orchard")]
471            orchard: vec![],
472            #[cfg(feature = "orchard")]
473            ironwood: vec![],
474        }
475    }
476
477    /// Fetches the nullifiers of the unspent notes being tracked by the given wallet.
478    pub fn unspent<DbT: WalletRead<AccountId = AccountId>>(
479        db_data: &DbT,
480    ) -> Result<Self, DbT::Error> {
481        Ok(Self::new(
482            db_data.get_sapling_nullifiers(NullifierQuery::Unspent)?,
483            #[cfg(feature = "orchard")]
484            db_data.get_orchard_nullifiers(NullifierQuery::Unspent)?,
485            #[cfg(feature = "orchard")]
486            db_data.get_ironwood_nullifiers(NullifierQuery::Unspent)?,
487        ))
488    }
489
490    /// Constructs a nullifier set from its constituent parts.
491    pub(crate) fn new(
492        sapling: Vec<(AccountId, sapling::Nullifier)>,
493        #[cfg(feature = "orchard")] orchard: Vec<(AccountId, orchard::note::Nullifier)>,
494        #[cfg(feature = "orchard")] ironwood: Vec<(AccountId, IronwoodNullifier)>,
495    ) -> Self {
496        Self {
497            sapling,
498            #[cfg(feature = "orchard")]
499            orchard,
500            #[cfg(feature = "orchard")]
501            ironwood,
502        }
503    }
504
505    /// Returns the Sapling nullifiers for notes that the wallet is tracking.
506    pub fn sapling(&self) -> &[(AccountId, sapling::Nullifier)] {
507        self.sapling.as_ref()
508    }
509
510    /// Returns the Orchard nullifiers for notes that the wallet is tracking.
511    #[cfg(feature = "orchard")]
512    pub fn orchard(&self) -> &[(AccountId, orchard::note::Nullifier)] {
513        self.orchard.as_ref()
514    }
515
516    /// Returns the Ironwood nullifiers for notes that the wallet is tracking.
517    #[cfg(feature = "orchard")]
518    pub fn ironwood(&self) -> &[(AccountId, IronwoodNullifier)] {
519        self.ironwood.as_ref()
520    }
521
522    /// Discards Sapling nullifiers from the tracked nullifier set, retaining only those that
523    /// satisfy the given predicate.
524    pub(crate) fn retain_sapling(&mut self, f: impl Fn(&(AccountId, sapling::Nullifier)) -> bool) {
525        self.sapling.retain(f);
526    }
527
528    /// Adds the given nullifiers to the tracked nullifier set.
529    pub(crate) fn extend_sapling(
530        &mut self,
531        nfs: impl IntoIterator<Item = (AccountId, sapling::Nullifier)>,
532    ) {
533        self.sapling.extend(nfs);
534    }
535
536    #[cfg(feature = "orchard")]
537    pub(crate) fn retain_orchard(
538        &mut self,
539        f: impl Fn(&(AccountId, orchard::note::Nullifier)) -> bool,
540    ) {
541        self.orchard.retain(f);
542    }
543
544    #[cfg(feature = "orchard")]
545    pub(crate) fn extend_orchard(
546        &mut self,
547        nfs: impl IntoIterator<Item = (AccountId, orchard::note::Nullifier)>,
548    ) {
549        self.orchard.extend(nfs);
550    }
551
552    #[cfg(feature = "orchard")]
553    pub(crate) fn retain_ironwood(&mut self, f: impl Fn(&(AccountId, IronwoodNullifier)) -> bool) {
554        self.ironwood.retain(f);
555    }
556
557    #[cfg(feature = "orchard")]
558    pub(crate) fn extend_ironwood(
559        &mut self,
560        nfs: impl IntoIterator<Item = (AccountId, IronwoodNullifier)>,
561    ) {
562        self.ironwood.extend(nfs);
563    }
564}
565
566impl<AccountId: Copy> Nullifiers<AccountId> {
567    /// Updates this set of unspent nullifiers based on the results of scanning a block.
568    ///
569    /// This is intended for use when scanning multiple sequential blocks in memory, prior
570    /// to updating the wallet's state (after which [`Self::unspent`] would produce the
571    /// same set).
572    ///
573    /// - Notes spent by the wallet in this block will have their nullifiers removed from
574    ///   the set, so that they are not matched again.
575    /// - Notes received by the wallet in this block will have their nullifiers added to
576    ///   the set, enabling spend detection in subsequent blocks.
577    pub fn update_with(&mut self, scanned_block: &ScannedBlock<AccountId>) {
578        let sapling_spent_nf: Vec<&sapling::Nullifier> = scanned_block
579            .transactions()
580            .iter()
581            .flat_map(|tx| tx.sapling_spends().iter().map(|spend| spend.nf()))
582            .collect();
583
584        self.retain_sapling(|(_, nf)| !sapling_spent_nf.contains(&nf));
585        self.extend_sapling(scanned_block.transactions().iter().flat_map(|tx| {
586            tx.sapling_outputs()
587                .iter()
588                .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
589        }));
590
591        #[cfg(feature = "orchard")]
592        {
593            let orchard_spent_nf: Vec<&orchard::note::Nullifier> = scanned_block
594                .transactions()
595                .iter()
596                .flat_map(|tx| tx.orchard_spends().iter().map(|spend| spend.nf()))
597                .collect();
598
599            self.retain_orchard(|(_, nf)| !orchard_spent_nf.contains(&nf));
600            self.extend_orchard(scanned_block.transactions().iter().flat_map(|tx| {
601                tx.orchard_outputs()
602                    .iter()
603                    .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
604            }));
605
606            let ironwood_spent_nf: Vec<&IronwoodNullifier> = scanned_block
607                .transactions()
608                .iter()
609                .flat_map(|tx| tx.ironwood_spends().iter().map(|spend| spend.nf()))
610                .collect();
611
612            self.retain_ironwood(|(_, nf)| !ironwood_spent_nf.contains(&nf));
613            self.extend_ironwood(scanned_block.transactions().iter().flat_map(|tx| {
614                tx.ironwood_outputs()
615                    .iter()
616                    .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
617            }));
618        }
619    }
620}
621
622/// The set of identifiers whose appearance in a scanned block reveals a spend of wallet funds:
623/// the [`Nullifiers`] of the wallet's unspent shielded notes, together with the outpoints of its
624/// unspent transparent outputs.
625///
626/// Transparent outpoints are held in a hash map, and matching them is not constant-time. This is
627/// not because the data is public — an outpoint is public, but the wallet's association with it
628/// is exactly what a wallet rotating addresses and shielding promptly needs to keep private.
629/// It is because the transparent path cannot deliver the property at this point alone:
630/// recognizing a transparent output requires resolving its address against the wallet's address
631/// set, and that lookup, whether a hash-map probe or a storage backend's indexed query, already
632/// takes time that depends on whether the address is the wallet's. Constant-time matching here
633/// would leave that larger channel open while costing a linear scan of what may be a large UTXO
634/// set. Closing it properly requires addressing both paths together.
635// TODO: Recognize transparent outputs and spends in constant time, across both the address
636// resolution and spend matching paths.
637// https://github.com/zcash/librustzcash/issues/2994
638pub struct SpendIdentifiers<AccountId> {
639    shielded: Nullifiers<AccountId>,
640    #[cfg(feature = "transparent-inputs")]
641    transparent: HashMap<OutPoint, AccountId>,
642}
643
644impl<AccountId> SpendIdentifiers<AccountId> {
645    /// Constructs a new empty set of spend identifiers.
646    pub fn empty() -> Self {
647        Self {
648            shielded: Nullifiers::empty(),
649            #[cfg(feature = "transparent-inputs")]
650            transparent: HashMap::new(),
651        }
652    }
653
654    /// Fetches the spend identifiers of the unspent outputs being tracked by the given wallet.
655    pub fn unspent<DbT: WalletRead<AccountId = AccountId>>(
656        db_data: &DbT,
657    ) -> Result<Self, DbT::Error> {
658        Ok(Self::new(
659            Nullifiers::unspent(db_data)?,
660            #[cfg(feature = "transparent-inputs")]
661            db_data.get_unspent_transparent_outpoints()?,
662        ))
663    }
664
665    /// Constructs a spend identifier set from its constituent parts.
666    pub(crate) fn new(
667        shielded: Nullifiers<AccountId>,
668        #[cfg(feature = "transparent-inputs")] transparent: HashMap<OutPoint, AccountId>,
669    ) -> Self {
670        Self {
671            shielded,
672            #[cfg(feature = "transparent-inputs")]
673            transparent,
674        }
675    }
676
677    /// Returns the nullifiers of the shielded notes that the wallet is tracking.
678    pub fn shielded(&self) -> &Nullifiers<AccountId> {
679        &self.shielded
680    }
681
682    /// Returns the outpoints of the unspent transparent outputs that the wallet is tracking,
683    /// each mapped to the account that received it.
684    #[cfg(feature = "transparent-inputs")]
685    pub fn transparent(&self) -> &HashMap<OutPoint, AccountId> {
686        &self.transparent
687    }
688}
689
690impl<AccountId: Copy> SpendIdentifiers<AccountId> {
691    /// Updates this set of spend identifiers based on the results of scanning a block.
692    ///
693    /// The shielded nullifiers are updated as by [`Nullifiers::update_with`]. Likewise, the
694    /// outpoints of transparent outputs the wallet spent in this block are removed, and those
695    /// of the transparent outputs it received are added, so that their spends are detected in
696    /// subsequent blocks.
697    pub fn update_with(&mut self, scanned_block: &ScannedBlock<AccountId>) {
698        self.shielded.update_with(scanned_block);
699
700        #[cfg(feature = "transparent-inputs")]
701        {
702            for spend in scanned_block
703                .transactions()
704                .iter()
705                .flat_map(|tx| tx.transparent_spends())
706            {
707                self.transparent.remove(spend.outpoint());
708            }
709
710            self.transparent.extend(
711                scanned_block
712                    .transactions()
713                    .iter()
714                    .flat_map(|tx| tx.transparent_outputs())
715                    .filter_map(|out| {
716                        out.recipient_account()
717                            .map(|account_id| (out.outpoint().clone(), *account_id))
718                    }),
719            );
720        }
721    }
722}
723
724/// Errors that may occur in chain scanning
725#[derive(Clone, Debug)]
726#[non_exhaustive]
727pub enum ScanError {
728    /// The encoding of a compact Sapling output or compact Orchard action was invalid.
729    EncodingInvalid {
730        at_height: BlockHeight,
731        txid: TxId,
732        pool_type: ShieldedPool,
733        index: usize,
734    },
735
736    /// The hash of the parent block given by a proposed new chain tip does not match the hash of
737    /// the current chain tip.
738    PrevHashMismatch { at_height: BlockHeight },
739
740    /// The block height field of the proposed new block is not equal to the height of the previous
741    /// block + 1.
742    BlockHeightDiscontinuity {
743        prev_height: BlockHeight,
744        new_height: BlockHeight,
745    },
746
747    /// The note commitment tree size for the given protocol at the proposed new block is not equal
748    /// to the size at the previous block plus the count of this block's outputs.
749    TreeSizeMismatch {
750        protocol: ShieldedPool,
751        at_height: BlockHeight,
752        given: u32,
753        computed: u32,
754    },
755
756    /// The size of the note commitment tree for the given protocol was not provided as part of a
757    /// [`CompactBlock`] being scanned, making it impossible to construct the nullifier for a
758    /// detected note.
759    TreeSizeUnknown {
760        protocol: ShieldedPool,
761        at_height: BlockHeight,
762    },
763
764    /// We were provided chain metadata for a block containing note commitment tree metadata
765    /// that is invalidated by the data in the block itself. This may be caused by the presence
766    /// of default values in the chain metadata.
767    TreeSizeInvalid {
768        protocol: ShieldedPool,
769        at_height: BlockHeight,
770    },
771
772    /// The size of the note commitment tree for the given protocol would exceed the
773    /// `u32` range as a result of applying the outputs in the block being scanned.
774    TreeSizeOverflow {
775        protocol: ShieldedPool,
776        at_height: BlockHeight,
777    },
778
779    /// The prevout reference of a compact transparent input could not be decoded.
780    ///
781    /// Scanning fails rather than skipping such an input: without its prevout the wallet cannot
782    /// determine whether the input spends one of its own outputs, and treating it as unrelated
783    /// would silently overstate the wallet's balance.
784    #[cfg(feature = "transparent-inputs")]
785    TransparentPrevoutInvalid {
786        at_height: BlockHeight,
787        txid: TxId,
788        index: usize,
789    },
790}
791
792impl ScanError {
793    /// Returns whether this error is the result of a failed continuity check
794    pub fn is_continuity_error(&self) -> bool {
795        match self {
796            EncodingInvalid { .. } => false,
797            PrevHashMismatch { .. } => true,
798            BlockHeightDiscontinuity { .. } => true,
799            TreeSizeMismatch { .. } => true,
800            TreeSizeUnknown { .. } => false,
801            TreeSizeInvalid { .. } => false,
802            TreeSizeOverflow { .. } => false,
803            #[cfg(feature = "transparent-inputs")]
804            TransparentPrevoutInvalid { .. } => false,
805        }
806    }
807
808    /// Returns the block height at which the scan error occurred
809    pub fn at_height(&self) -> BlockHeight {
810        match self {
811            EncodingInvalid { at_height, .. } => *at_height,
812            PrevHashMismatch { at_height } => *at_height,
813            BlockHeightDiscontinuity { new_height, .. } => *new_height,
814            TreeSizeMismatch { at_height, .. } => *at_height,
815            TreeSizeUnknown { at_height, .. } => *at_height,
816            TreeSizeInvalid { at_height, .. } => *at_height,
817            TreeSizeOverflow { at_height, .. } => *at_height,
818            #[cfg(feature = "transparent-inputs")]
819            TransparentPrevoutInvalid { at_height, .. } => *at_height,
820        }
821    }
822}
823
824impl fmt::Display for ScanError {
825    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
826        match &self {
827            EncodingInvalid {
828                txid,
829                pool_type,
830                index,
831                ..
832            } => write!(
833                f,
834                "{pool_type:?} output {index} of transaction {txid} was improperly encoded."
835            ),
836            PrevHashMismatch { at_height } => write!(
837                f,
838                "The parent hash of proposed block does not correspond to the block hash at height {at_height}."
839            ),
840            BlockHeightDiscontinuity {
841                prev_height,
842                new_height,
843            } => {
844                write!(
845                    f,
846                    "Block height discontinuity at height {new_height}; previous height was: {prev_height}"
847                )
848            }
849            TreeSizeMismatch {
850                protocol,
851                at_height,
852                given,
853                computed,
854            } => {
855                write!(
856                    f,
857                    "The {protocol:?} note commitment tree size provided by a compact block did not match the expected size at height {at_height}; given {given}, expected {computed}"
858                )
859            }
860            TreeSizeUnknown {
861                protocol,
862                at_height,
863            } => {
864                write!(
865                    f,
866                    "Unable to determine {protocol:?} note commitment tree size at height {at_height}"
867                )
868            }
869            TreeSizeInvalid {
870                protocol,
871                at_height,
872            } => {
873                write!(
874                    f,
875                    "Received invalid (potentially default) {protocol:?} note commitment tree size metadata at height {at_height}"
876                )
877            }
878            TreeSizeOverflow {
879                protocol,
880                at_height,
881            } => {
882                write!(
883                    f,
884                    "The {protocol:?} note commitment tree size at height {at_height} would exceed the `u32` range."
885                )
886            }
887            #[cfg(feature = "transparent-inputs")]
888            TransparentPrevoutInvalid {
889                txid,
890                index,
891                at_height,
892            } => {
893                write!(
894                    f,
895                    "The prevout of transparent input {index} of transaction {txid} at height {at_height} was improperly encoded."
896                )
897            }
898        }
899    }
900}
901
902impl core::error::Error for ScanError {}
903
904/// Errors that can occur while scanning a block, including those arising from the wallet
905/// lookups that transparent output detection requires.
906#[derive(Debug)]
907#[non_exhaustive]
908pub enum ScanBlockError<E> {
909    /// A structural or continuity error in the block being scanned.
910    Scan(ScanError),
911    /// An error occurred while looking up the wallet account associated with a
912    /// transparent address.
913    AddressLookup(E),
914}
915
916impl<E> From<ScanError> for ScanBlockError<E> {
917    fn from(e: ScanError) -> Self {
918        ScanBlockError::Scan(e)
919    }
920}
921
922impl<E: fmt::Display> fmt::Display for ScanBlockError<E> {
923    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
924        match self {
925            ScanBlockError::Scan(e) => write!(f, "Error scanning block: {e}"),
926            ScanBlockError::AddressLookup(e) => write!(
927                f,
928                "Error looking up the wallet account for a transparent address: {e}"
929            ),
930        }
931    }
932}
933
934impl<E: std::error::Error + 'static> std::error::Error for ScanBlockError<E> {
935    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
936        match self {
937            ScanBlockError::Scan(e) => Some(e),
938            ScanBlockError::AddressLookup(e) => Some(e),
939        }
940    }
941}
942
943/// Scans a [`CompactBlock`] with a set of [`ScanningKeys`].
944///
945/// Returns a vector of [`WalletTx`]s decryptable by any of the given keys. If an output is
946/// decrypted by a full viewing key, the nullifiers of that output will also be computed.
947///
948/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
949/// [`WalletTx`]: crate::wallet::WalletTx
950pub fn scan_block<P, AccountId, IvkTag, E>(
951    params: &P,
952    block: CompactBlock,
953    scanning_keys: &ScanningKeys<AccountId, IvkTag>,
954    spend_ids: &SpendIdentifiers<AccountId>,
955    prior_block_metadata: Option<&BlockMetadata>,
956    #[cfg(feature = "transparent-inputs")] find_account_for_address: impl Fn(
957        &TransparentAddress,
958    ) -> Result<
959        Option<(AccountId, Option<TransparentKeyScope>)>,
960        E,
961    >,
962) -> Result<ScannedBlock<AccountId>, ScanBlockError<E>>
963where
964    P: consensus::Parameters + Send + 'static,
965    AccountId: Default + Eq + Hash + ConditionallySelectable + Send + Sync + 'static,
966    IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
967{
968    compact::scan_block_with_runners::<_, _, _, (), (), (), _>(
969        params,
970        block,
971        scanning_keys,
972        spend_ids,
973        prior_block_metadata,
974        None,
975        #[cfg(feature = "transparent-inputs")]
976        find_account_for_address,
977    )
978}
979
980/// Tracks the scanner's position within the note commitment trees, in order to calculate
981/// received note positions.
982///
983/// Construction for compact blocks is handled in the [`compact`] submodule, and for full
984/// blocks in the [`full`] submodule; each provides its own `PositionTracker` constructor.
985struct PositionTracker {
986    sapling_tree_position: u32,
987    sapling_final_tree_size: u32,
988    #[cfg(feature = "orchard")]
989    orchard_tree_position: u32,
990    #[cfg(feature = "orchard")]
991    orchard_final_tree_size: u32,
992    #[cfg(feature = "orchard")]
993    ironwood_tree_position: u32,
994    #[cfg(feature = "orchard")]
995    ironwood_final_tree_size: u32,
996}
997
998impl PositionTracker {
999    fn sapling_note_position(&self, output_idx: usize) -> Position {
1000        Position::from(u64::from(
1001            self.sapling_tree_position + u32::try_from(output_idx).unwrap(),
1002        ))
1003    }
1004
1005    #[cfg(feature = "orchard")]
1006    fn orchard_note_position(&self, output_idx: usize) -> Position {
1007        Position::from(u64::from(
1008            self.orchard_tree_position + u32::try_from(output_idx).unwrap(),
1009        ))
1010    }
1011
1012    #[cfg(feature = "orchard")]
1013    fn ironwood_note_position(&self, output_idx: usize) -> Position {
1014        Position::from(u64::from(
1015            self.ironwood_tree_position + u32::try_from(output_idx).unwrap(),
1016        ))
1017    }
1018}
1019
1020/// Check for spent notes. The comparison against known-unspent nullifiers is done
1021/// in constant time.
1022fn find_spent<'a, I, AccountId, Spend, Nf, WS>(
1023    spends: I,
1024    nullifiers: &[(AccountId, Nf)],
1025    extract_nf: impl Fn(&Spend) -> Nf,
1026    construct_wallet_spend: impl Fn(usize, Nf, AccountId) -> WS,
1027) -> (Vec<WS>, Vec<Nf>)
1028where
1029    I: IntoIterator<Item = &'a Spend>,
1030    I::IntoIter: ExactSizeIterator,
1031    Spend: 'a,
1032    AccountId: ConditionallySelectable + Default,
1033    Nf: ConstantTimeEq + Copy,
1034{
1035    let spends = spends.into_iter();
1036    // TODO: this is O(|nullifiers| * |notes|); does using constant-time operations here really
1037    // make sense?
1038    let mut found_spent = vec![];
1039    let mut unlinked_nullifiers = Vec::with_capacity(spends.len());
1040    for (index, spend) in spends.enumerate() {
1041        let spend_nf = extract_nf(spend);
1042
1043        // Find whether any tracked nullifier that matches this spend, and produce a
1044        // WalletShieldedSpend in constant time.
1045        let ct_spend = nullifiers
1046            .iter()
1047            .map(|&(account, nf)| CtOption::new(account, nf.ct_eq(&spend_nf)))
1048            .fold(
1049                CtOption::new(AccountId::default(), 0.into()),
1050                |first, next| CtOption::conditional_select(&next, &first, first.is_some()),
1051            )
1052            .map(|account| construct_wallet_spend(index, spend_nf, account));
1053
1054        if let Some(spend) = ct_spend.into() {
1055            found_spent.push(spend);
1056        } else {
1057            // This nullifier didn't match any we are currently tracking; save it in
1058            // case it matches an earlier block range we haven't scanned yet.
1059            unlinked_nullifiers.push(spend_nf);
1060        }
1061    }
1062
1063    (found_spent, unlinked_nullifiers)
1064}
1065
1066/// Partitions a transaction's transparent inputs into those that spend an output the wallet is
1067/// tracking and those that do not, returning the wallet's spends alongside the prevouts of the
1068/// remaining inputs.
1069///
1070/// The unmatched prevouts are not known to be unrelated to the wallet: an input spending an output
1071/// created in a block range the wallet has not yet scanned cannot be recognized here, because the
1072/// wallet does not yet know the output exists. They are returned so that the caller can record
1073/// them against the spending transaction and resolve the spend if that output is discovered later.
1074///
1075/// Unlike [`find_spent`], the comparison is not constant-time; see [`SpendIdentifiers`] for why
1076/// the transparent path does not attempt that property.
1077#[cfg(feature = "transparent-inputs")]
1078fn find_transparent_spends<AccountId: Copy>(
1079    prevouts: impl IntoIterator<Item = OutPoint>,
1080    tracked: &HashMap<OutPoint, AccountId>,
1081) -> (Vec<WalletTransparentSpend<AccountId>>, Vec<OutPoint>) {
1082    let mut found_spent = vec![];
1083    let mut unlinked_prevouts = vec![];
1084
1085    for (index, prevout) in prevouts.into_iter().enumerate() {
1086        match tracked.get(&prevout) {
1087            Some(account_id) => found_spent.push(WalletTransparentSpend::from_parts(
1088                index,
1089                prevout,
1090                *account_id,
1091            )),
1092            None => unlinked_prevouts.push(prevout),
1093        }
1094    }
1095
1096    (found_spent, unlinked_prevouts)
1097}
1098
1099#[allow(clippy::too_many_arguments)]
1100#[allow(clippy::type_complexity)]
1101fn find_received<
1102    AccountId: Copy + Eq + Hash,
1103    D: BatchDomain,
1104    M,
1105    Nf,
1106    IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
1107    SK: ScanningKeyOps<D, AccountId, Nf>,
1108    Output: ShieldedOutput<D>,
1109    NoteCommitment,
1110    Note,
1111>(
1112    block_height: BlockHeight,
1113    last_commitments_in_block: bool,
1114    txid: TxId,
1115    note_position: impl Fn(usize) -> Position,
1116    keys: &HashMap<IvkTag, SK>,
1117    spent_from_accounts: &HashSet<AccountId>,
1118    decoded: &[(D, Output)],
1119    batch_results: Option<impl FnOnce(TxId) -> HashMap<usize, DecryptedOutput<IvkTag, D, M>>>,
1120    decrypt_inline: impl FnOnce(
1121        &[D::IncomingViewingKey],
1122        &[(D, Output)],
1123    ) -> Vec<Option<((D::Note, D::Recipient, M), usize)>>,
1124    extract_note_commitment: impl Fn(&Output) -> NoteCommitment,
1125    enrich_note: impl Fn(D::Note) -> Note,
1126) -> (
1127    Vec<WalletOutput<Note, Nf, AccountId>>,
1128    Vec<(NoteCommitment, Retention<BlockHeight>)>,
1129) {
1130    // Check for incoming notes while incrementing tree and witnesses
1131    let (decrypted_opts, decrypted_len) = if let Some(collect_results) = batch_results {
1132        let mut decrypted = collect_results(txid);
1133        let decrypted_len = decrypted.len();
1134        (
1135            (0..decoded.len())
1136                .map(|i| {
1137                    decrypted
1138                        .remove(&i)
1139                        .map(|d_out| (d_out.ivk_tag, d_out.note))
1140                })
1141                .collect::<Vec<_>>(),
1142            decrypted_len,
1143        )
1144    } else {
1145        let mut ivks = Vec::with_capacity(keys.len());
1146        let mut ivk_lookup = Vec::with_capacity(keys.len());
1147        for (key_id, key) in keys.iter() {
1148            ivks.push(key.prepare());
1149            ivk_lookup.push(key_id);
1150        }
1151
1152        let mut decrypted_len = 0;
1153        (
1154            decrypt_inline(&ivks, decoded)
1155                .into_iter()
1156                .map(|v| {
1157                    v.map(|((note, _, _), ivk_idx)| {
1158                        decrypted_len += 1;
1159                        (*ivk_lookup[ivk_idx], note)
1160                    })
1161                })
1162                .collect::<Vec<_>>(),
1163            decrypted_len,
1164        )
1165    };
1166
1167    let mut shielded_outputs = Vec::with_capacity(decrypted_len);
1168    let mut note_commitments = Vec::with_capacity(decoded.len());
1169    for (output_idx, ((_, output), decrypted_note)) in
1170        decoded.iter().zip(decrypted_opts).enumerate()
1171    {
1172        // Collect block note commitments
1173        let node = extract_note_commitment(output);
1174        // If the commitment is the last in the block, ensure that is retained as a checkpoint
1175        let is_checkpoint = output_idx + 1 == decoded.len() && last_commitments_in_block;
1176        let retention = match (decrypted_note.is_some(), is_checkpoint) {
1177            (is_marked, true) => Retention::Checkpoint {
1178                id: block_height,
1179                marking: if is_marked {
1180                    Marking::Marked
1181                } else {
1182                    Marking::None
1183                },
1184            },
1185            (true, false) => Retention::Marked,
1186            (false, false) => Retention::Ephemeral,
1187        };
1188
1189        if let Some((key_id, note)) = decrypted_note {
1190            let key = keys
1191                .get(&key_id)
1192                .expect("Key is available for decrypted output");
1193
1194            // A note is marked as "change" if the account that received it
1195            // also spent notes in the same transaction. This will catch,
1196            // for instance:
1197            // - Change created by spending fractions of notes.
1198            // - Notes created by consolidation transactions.
1199            // - Notes sent from one account to itself.
1200            let is_change = spent_from_accounts.contains(key.account_id());
1201            let note_commitment_tree_position = note_position(output_idx);
1202            let nf = key.nf(&note, note_commitment_tree_position);
1203
1204            shielded_outputs.push(WalletOutput::from_parts(
1205                output_idx,
1206                output.ephemeral_key(),
1207                enrich_note(note),
1208                is_change,
1209                note_commitment_tree_position,
1210                nf,
1211                *key.account_id(),
1212                key.key_scope(),
1213            ));
1214        }
1215
1216        note_commitments.push((node, retention))
1217    }
1218
1219    (shielded_outputs, note_commitments)
1220}
1221
1222#[cfg(any(test, feature = "test-dependencies"))]
1223pub mod testing {
1224    use group::{
1225        GroupEncoding,
1226        ff::{Field, PrimeField},
1227    };
1228    use rand::{Rng, rand_core::UnwrapErr, rngs::SysRng};
1229    use sapling::{
1230        Nullifier,
1231        constants::SPENDING_KEY_GENERATOR,
1232        note_encryption::{COMPACT_NOTE_SIZE, SaplingDomain, sapling_note_encryption},
1233        util::generate_random_rseed,
1234        value::NoteValue,
1235        zip32::DiversifiableFullViewingKey,
1236    };
1237    use zcash_note_encryption::Domain;
1238    use zcash_primitives::{
1239        block::BlockHash, transaction::components::sapling::zip212_enforcement,
1240    };
1241    use zcash_protocol::{
1242        consensus::{BlockHeight, Network},
1243        memo::MemoBytes,
1244        value::Zatoshis,
1245    };
1246
1247    use crate::proto::compact_formats::{
1248        self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
1249    };
1250
1251    fn random_compact_tx(mut rng: impl Rng) -> CompactTx {
1252        let fake_nf = {
1253            let mut nf = vec![0; 32];
1254            rng.fill_bytes(&mut nf);
1255            nf
1256        };
1257        let fake_cmu = {
1258            let fake_cmu = bls12_381::Scalar::random(&mut rng);
1259            fake_cmu.to_repr().to_vec()
1260        };
1261        let fake_epk = {
1262            let mut buffer = [0; 64];
1263            rng.fill_bytes(&mut buffer);
1264            let fake_esk = jubjub::Fr::from_bytes_wide(&buffer);
1265            let fake_epk = SPENDING_KEY_GENERATOR * fake_esk;
1266            fake_epk.to_bytes().to_vec()
1267        };
1268        let cspend = CompactSaplingSpend { nf: fake_nf };
1269        let cout = CompactSaplingOutput {
1270            cmu: fake_cmu,
1271            ephemeral_key: fake_epk,
1272            ciphertext: vec![0; COMPACT_NOTE_SIZE],
1273        };
1274        let mut ctx = CompactTx::default();
1275        let mut txid = vec![0; 32];
1276        rng.fill_bytes(&mut txid);
1277        ctx.txid = txid;
1278        ctx.spends.push(cspend);
1279        ctx.outputs.push(cout);
1280        ctx
1281    }
1282
1283    /// Create a fake CompactBlock at the given height, with a transaction containing a
1284    /// single spend of the given nullifier and a single output paying the given address.
1285    /// Returns the CompactBlock.
1286    ///
1287    /// Set `initial_tree_sizes` to `None` to simulate a `CompactBlock` retrieved
1288    /// from a `lightwalletd` that is not currently tracking note commitment tree sizes.
1289    pub fn fake_compact_block(
1290        height: BlockHeight,
1291        prev_hash: BlockHash,
1292        nf: Nullifier,
1293        dfvk: &DiversifiableFullViewingKey,
1294        value: Zatoshis,
1295        tx_after: bool,
1296        initial_tree_sizes: Option<(u32, u32)>,
1297    ) -> CompactBlock {
1298        let zip212_enforcement = zip212_enforcement(&Network::TestNetwork, height);
1299        let to = dfvk.default_address().1;
1300
1301        // Create a fake Note for the account
1302        let mut rng = UnwrapErr(SysRng);
1303        let rseed = generate_random_rseed(zip212_enforcement, &mut rng);
1304        let note = sapling::Note::from_parts(to, NoteValue::from_raw(value.into()), rseed);
1305        let encryptor = sapling_note_encryption(
1306            Some(dfvk.fvk().ovk),
1307            note.clone(),
1308            MemoBytes::empty().into_bytes(),
1309            &mut rng,
1310        );
1311        let cmu = note.cmu().to_bytes().to_vec();
1312        let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec();
1313        let enc_ciphertext = encryptor.encrypt_note_plaintext();
1314
1315        // Create a fake CompactBlock containing the note
1316        let mut cb = CompactBlock {
1317            hash: {
1318                let mut hash = vec![0; 32];
1319                rng.fill_bytes(&mut hash);
1320                hash
1321            },
1322            prev_hash: prev_hash.0.to_vec(),
1323            height: height.into(),
1324            ..Default::default()
1325        };
1326
1327        // Add a random Sapling tx before ours
1328        {
1329            let mut tx = random_compact_tx(&mut rng);
1330            tx.index = cb.vtx.len() as u64;
1331            cb.vtx.push(tx);
1332        }
1333
1334        let cspend = CompactSaplingSpend { nf: nf.0.to_vec() };
1335        let cout = CompactSaplingOutput {
1336            cmu,
1337            ephemeral_key,
1338            ciphertext: enc_ciphertext.0[..COMPACT_NOTE_SIZE].to_vec(),
1339        };
1340        let mut ctx = CompactTx::default();
1341        let mut txid = vec![0; 32];
1342        rng.fill_bytes(&mut txid);
1343        ctx.txid = txid;
1344        ctx.spends.push(cspend);
1345        ctx.outputs.push(cout);
1346        ctx.index = cb.vtx.len() as u64;
1347        cb.vtx.push(ctx);
1348
1349        // Optionally add another random Sapling tx after ours
1350        if tx_after {
1351            let mut tx = random_compact_tx(&mut rng);
1352            tx.index = cb.vtx.len() as u64;
1353            cb.vtx.push(tx);
1354        }
1355
1356        cb.chain_metadata =
1357            initial_tree_sizes.map(|(initial_sapling_tree_size, initial_orchard_tree_size)| {
1358                compact::ChainMetadata {
1359                    sapling_commitment_tree_size: initial_sapling_tree_size
1360                        + cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::<u32>(),
1361                    orchard_commitment_tree_size: initial_orchard_tree_size
1362                        + cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::<u32>(),
1363                    // The test framework does not generate Ironwood notes.
1364                    ironwood_commitment_tree_size: 0,
1365                }
1366            });
1367
1368        cb
1369    }
1370}
1371
1372#[cfg(test)]
1373mod tests {
1374    use std::{sync::Arc, thread::spawn};
1375
1376    use super::ScanningKeys;
1377
1378    #[test]
1379    fn arc_scanning_keys() {
1380        let keys = Arc::new(ScanningKeys::<(), ()>::empty());
1381        spawn(move || {
1382            let _ = keys.sapling().get(&());
1383        });
1384    }
1385}