Skip to main content

zcash_client_backend/scanning/
full.rs

1use std::collections::{HashMap, HashSet};
2use std::convert::TryFrom;
3use std::fmt;
4use std::hash::Hash;
5
6use incrementalmerkletree::Retention;
7use sapling::note_encryption::SaplingDomain;
8use subtle::ConditionallySelectable;
9use tracing::{debug, trace, warn};
10
11use zcash_note_encryption::{Domain, batch};
12use zcash_primitives::{
13    block::{Block, BlockHeader},
14    transaction::{Transaction, components::sapling::zip212_enforcement},
15};
16use zcash_protocol::{
17    ShieldedPool,
18    consensus::{self, BlockHeight, NetworkUpgrade, TxIndex},
19};
20
21use super::{
22    PositionTracker, ScanError, ScanningKeys, SpendIdentifiers, find_received, find_spent,
23};
24
25#[cfg(feature = "transparent-inputs")]
26use super::find_transparent_spends;
27
28#[doc(inline)]
29pub use super::ScanBlockError;
30use crate::{
31    data_api::{
32        BlockMetadata, ScannedBlock, ScannedBundles, ll::wallet::detect_wallet_transparent_outputs,
33    },
34    scan::{Batch, BatchReceiver, BatchRunner, DecryptedOutput, FullDecryptor, Tasks},
35    wallet::{WalletSpend, WalletTx},
36};
37
38#[cfg(feature = "orchard")]
39use orchard::{note_encryption::OrchardDomain, primitives::redpallas, tree::MerkleHashOrchard};
40
41#[cfg(feature = "orchard")]
42use super::IronwoodDomain;
43
44#[cfg(not(feature = "orchard"))]
45use std::marker::PhantomData;
46
47#[cfg(feature = "transparent-inputs")]
48use transparent::{address::TransparentAddress, keys::TransparentKeyScope};
49
50/// The default number of outputs at which a batch runner immediately flushes a batch.
51pub(crate) const DEFAULT_BATCH_SIZE_THRESHOLD: usize = 200;
52
53type TaggedSaplingBatch<IvkTag> = Batch<
54    IvkTag,
55    SaplingDomain,
56    sapling::bundle::OutputDescription<sapling::bundle::GrothProofBytes>,
57    FullDecryptor,
58>;
59type TaggedSaplingBatchRunner<IvkTag, Tasks> = BatchRunner<
60    IvkTag,
61    SaplingDomain,
62    sapling::bundle::OutputDescription<sapling::bundle::GrothProofBytes>,
63    FullDecryptor,
64    Tasks,
65>;
66
67#[cfg(feature = "orchard")]
68type TaggedOrchardBatch<IvkTag> = Batch<
69    IvkTag,
70    OrchardDomain,
71    orchard::Action<redpallas::Signature<redpallas::SpendAuth>>,
72    FullDecryptor,
73>;
74#[cfg(feature = "orchard")]
75type TaggedOrchardBatchRunner<IvkTag, Tasks> = BatchRunner<
76    IvkTag,
77    OrchardDomain,
78    orchard::Action<redpallas::Signature<redpallas::SpendAuth>>,
79    FullDecryptor,
80    Tasks,
81>;
82
83// Ironwood outputs are decrypted under the Ironwood note-encryption domain, which is distinct from
84// the Orchard domain (it accepts version 3 note plaintexts), so an Ironwood batch is a distinct
85// type from an Orchard batch and requires its own task type.
86#[cfg(feature = "orchard")]
87type TaggedIronwoodBatch<IvkTag> = Batch<
88    IvkTag,
89    IronwoodDomain,
90    orchard::Action<redpallas::Signature<redpallas::SpendAuth>>,
91    FullDecryptor,
92>;
93#[cfg(feature = "orchard")]
94type TaggedIronwoodBatchRunner<IvkTag, Tasks> = BatchRunner<
95    IvkTag,
96    IronwoodDomain,
97    orchard::Action<redpallas::Signature<redpallas::SpendAuth>>,
98    FullDecryptor,
99    Tasks,
100>;
101
102pub(crate) trait SaplingTasks<IvkTag>: Tasks<TaggedSaplingBatch<IvkTag>> {}
103impl<IvkTag, T: Tasks<TaggedSaplingBatch<IvkTag>>> SaplingTasks<IvkTag> for T {}
104
105#[cfg(not(feature = "orchard"))]
106pub(crate) trait OrchardTasks<IvkTag> {}
107#[cfg(not(feature = "orchard"))]
108impl<IvkTag, T> OrchardTasks<IvkTag> for T {}
109
110#[cfg(feature = "orchard")]
111pub(crate) trait OrchardTasks<IvkTag>: Tasks<TaggedOrchardBatch<IvkTag>> {}
112#[cfg(feature = "orchard")]
113impl<IvkTag, T: Tasks<TaggedOrchardBatch<IvkTag>>> OrchardTasks<IvkTag> for T {}
114
115#[cfg(not(feature = "orchard"))]
116pub(crate) trait IronwoodTasks<IvkTag> {}
117#[cfg(not(feature = "orchard"))]
118impl<IvkTag, T> IronwoodTasks<IvkTag> for T {}
119
120#[cfg(feature = "orchard")]
121pub(crate) trait IronwoodTasks<IvkTag>: Tasks<TaggedIronwoodBatch<IvkTag>> {}
122#[cfg(feature = "orchard")]
123impl<IvkTag, T: Tasks<TaggedIronwoodBatch<IvkTag>>> IronwoodTasks<IvkTag> for T {}
124
125pub(crate) struct BatchRunners<
126    IvkTag,
127    TS: SaplingTasks<IvkTag>,
128    TO: OrchardTasks<IvkTag>,
129    TI: IronwoodTasks<IvkTag>,
130> {
131    sapling: TaggedSaplingBatchRunner<IvkTag, TS>,
132    #[cfg(feature = "orchard")]
133    orchard: TaggedOrchardBatchRunner<IvkTag, TO>,
134    #[cfg(feature = "orchard")]
135    ironwood: TaggedIronwoodBatchRunner<IvkTag, TI>,
136    #[cfg(not(feature = "orchard"))]
137    orchard: PhantomData<(TO, TI)>,
138}
139
140impl<IvkTag, TS, TO, TI> BatchRunners<IvkTag, TS, TO, TI>
141where
142    IvkTag: Clone + Send + 'static,
143    TS: SaplingTasks<IvkTag>,
144    TO: OrchardTasks<IvkTag>,
145    TI: IronwoodTasks<IvkTag>,
146{
147    /// Constructs a fresh set of batch runners that will trial-decrypt outputs using the
148    /// given scanning keys.
149    ///
150    /// Each per-pool runner immediately flushes a batch once it has accumulated the
151    /// corresponding `*_batch_size_threshold` number of outputs.
152    pub(crate) fn for_keys<AccountId>(
153        sapling_batch_size_threshold: usize,
154        #[cfg(feature = "orchard")] orchard_batch_size_threshold: usize,
155        #[cfg(feature = "orchard")] ironwood_batch_size_threshold: usize,
156        scanning_keys: &ScanningKeys<AccountId, IvkTag>,
157    ) -> Self {
158        BatchRunners {
159            sapling: BatchRunner::new(
160                sapling_batch_size_threshold,
161                scanning_keys
162                    .sapling()
163                    .iter()
164                    .map(|(id, key)| (id.clone(), key.prepare())),
165            ),
166            #[cfg(feature = "orchard")]
167            orchard: BatchRunner::new(
168                orchard_batch_size_threshold,
169                scanning_keys
170                    .orchard()
171                    .iter()
172                    .map(|(id, key)| (id.clone(), key.prepare())),
173            ),
174            #[cfg(feature = "orchard")]
175            ironwood: BatchRunner::new(
176                ironwood_batch_size_threshold,
177                scanning_keys
178                    .ironwood()
179                    .iter()
180                    .map(|(id, key)| (id.clone(), key.prepare())),
181            ),
182            #[cfg(not(feature = "orchard"))]
183            orchard: PhantomData,
184        }
185    }
186
187    /// Adds the given transaction's shielded outputs to the various batch runners,
188    /// returning a [`PendingBatch`] from which the decryption results can be awaited.
189    ///
190    /// `height` is the height of the block containing the transaction, or the mempool
191    /// height (chain tip height + 1) for a mempool transaction.
192    ///
193    /// After adding the outputs, any accumulated batch of sufficient size is run on the
194    /// global threadpool. Subsequent calls to `Self::process_transaction` will accumulate
195    /// outputs into new batches.
196    #[tracing::instrument(skip_all, fields(height = u32::from(height)))]
197    pub(crate) fn process_transaction<P>(
198        &mut self,
199        params: &P,
200        height: BlockHeight,
201        tx: Transaction,
202    ) -> PendingBatch<IvkTag>
203    where
204        P: consensus::Parameters + Send + 'static,
205        IvkTag: Copy + Send + 'static,
206    {
207        let zip212_enforcement = zip212_enforcement(params, height);
208
209        let sapling_batch = tx.sapling_bundle().map(|bundle| {
210            self.sapling.process_outputs(
211                |_| SaplingDomain::new(zip212_enforcement),
212                bundle.shielded_outputs().iter().cloned(),
213            )
214        });
215
216        #[cfg(feature = "orchard")]
217        let orchard_batch = tx.orchard_bundle().map(|bundle| {
218            self.orchard
219                .process_outputs(OrchardDomain::for_action, bundle.actions().iter().cloned())
220        });
221
222        #[cfg(feature = "orchard")]
223        let ironwood_batch = tx.ironwood_bundle().map(|bundle| {
224            self.ironwood
225                .process_outputs(IronwoodDomain::for_action, bundle.actions().iter().cloned())
226        });
227
228        PendingBatch {
229            tx,
230            sapling_batch,
231            #[cfg(feature = "orchard")]
232            orchard_batch,
233            #[cfg(feature = "orchard")]
234            ironwood_batch,
235        }
236    }
237
238    /// Runs the currently accumulated batches on the global threadpool.
239    ///
240    /// Subsequent calls to [`Self::process_transaction`] will be accumulated into new batches.
241    pub(crate) fn flush(&mut self) {
242        self.sapling.flush();
243        #[cfg(feature = "orchard")]
244        self.orchard.flush();
245        #[cfg(feature = "orchard")]
246        self.ironwood.flush();
247    }
248}
249
250/// A pending batch decryption result for a single transaction.
251pub(crate) struct PendingBatch<IvkTag> {
252    tx: Transaction,
253    sapling_batch: Option<BatchReceiver<IvkTag, SaplingDomain, <SaplingDomain as Domain>::Memo>>,
254    #[cfg(feature = "orchard")]
255    orchard_batch: Option<BatchReceiver<IvkTag, OrchardDomain, <OrchardDomain as Domain>::Memo>>,
256    #[cfg(feature = "orchard")]
257    ironwood_batch: Option<BatchReceiver<IvkTag, IronwoodDomain, <IronwoodDomain as Domain>::Memo>>,
258}
259
260impl<IvkTag> PendingBatch<IvkTag> {
261    /// Blocks until the results of the batch are ready.
262    pub(crate) fn wait(self) -> BatchResult<IvkTag> {
263        BatchResult {
264            tx: self.tx,
265            sapling: self
266                .sapling_batch
267                .map(|b| b.into_results())
268                .unwrap_or_default(),
269            #[cfg(feature = "orchard")]
270            orchard: self
271                .orchard_batch
272                .map(|b| b.into_results())
273                .unwrap_or_default(),
274            #[cfg(feature = "orchard")]
275            ironwood: self
276                .ironwood_batch
277                .map(|b| b.into_results())
278                .unwrap_or_default(),
279        }
280    }
281
282    /// Waits, without blocking the current thread, until the results of the batch are
283    /// ready.
284    ///
285    /// This is the asynchronous counterpart of [`Self::wait`].
286    #[cfg(feature = "sync-decryptor")]
287    pub(crate) async fn wait_async(self) -> BatchResult<IvkTag> {
288        let sapling = match self.sapling_batch {
289            Some(b) => b.into_results_async().await,
290            None => HashMap::new(),
291        };
292        #[cfg(feature = "orchard")]
293        let orchard = match self.orchard_batch {
294            Some(b) => b.into_results_async().await,
295            None => HashMap::new(),
296        };
297        #[cfg(feature = "orchard")]
298        let ironwood = match self.ironwood_batch {
299            Some(b) => b.into_results_async().await,
300            None => HashMap::new(),
301        };
302        BatchResult {
303            tx: self.tx,
304            sapling,
305            #[cfg(feature = "orchard")]
306            orchard,
307            #[cfg(feature = "orchard")]
308            ironwood,
309        }
310    }
311}
312
313/// The result of batch-decrypting a single transaction.
314///
315/// This is an opaque value, produced by [`decrypt_block`] (or the `sync::decryptor`
316/// engine) and consumed by [`scan_block`]; it is not intended to be inspected directly.
317pub struct BatchResult<IvkTag> {
318    tx: Transaction,
319    sapling:
320        HashMap<usize, DecryptedOutput<IvkTag, SaplingDomain, <SaplingDomain as Domain>::Memo>>,
321    #[cfg(feature = "orchard")]
322    orchard:
323        HashMap<usize, DecryptedOutput<IvkTag, OrchardDomain, <OrchardDomain as Domain>::Memo>>,
324    #[cfg(feature = "orchard")]
325    ironwood:
326        HashMap<usize, DecryptedOutput<IvkTag, IronwoodDomain, <IronwoodDomain as Domain>::Memo>>,
327}
328
329/// Decrypts a block with a set of [`ScanningKeys`].
330///
331/// This is an inline, synchronous alternative to the batch decryption engine in the
332/// `sync::decryptor` module (available behind the `sync-decryptor` feature flag): it
333/// internally spins up batch runners to process the block, and discards them afterwards.
334///
335/// This is the first half of block scanning. Pass the result to [`scan_block`] for the
336/// second half.
337pub fn decrypt_block<P, AccountId, IvkTag>(
338    params: &P,
339    block: Block,
340    scanning_keys: &ScanningKeys<AccountId, IvkTag>,
341) -> (BlockHeader, Vec<BatchResult<IvkTag>>)
342where
343    P: consensus::Parameters + Send + 'static,
344    IvkTag: Copy + Send + 'static,
345{
346    let mut runners = BatchRunners::<_, (), (), ()>::for_keys(
347        DEFAULT_BATCH_SIZE_THRESHOLD,
348        #[cfg(feature = "orchard")]
349        DEFAULT_BATCH_SIZE_THRESHOLD,
350        #[cfg(feature = "orchard")]
351        DEFAULT_BATCH_SIZE_THRESHOLD,
352        scanning_keys,
353    );
354
355    let mined_height = block.claimed_height();
356    let (header, vtx) = block.into_parts();
357    let batches = vtx
358        .into_iter()
359        .map(|tx| runners.process_transaction(params, mined_height, tx))
360        .collect::<Vec<_>>();
361
362    // Flush any partially-accumulated batch so that every batch is actually run;
363    // otherwise `PendingBatch::wait` would block forever waiting on a batch that
364    // never started.
365    runners.flush();
366
367    let vtx = batches
368        .into_iter()
369        .map(|batch| batch.wait())
370        .collect::<Vec<_>>();
371
372    (header, vtx)
373}
374
375/// Scans a block with a set of [`ScanningKeys`].
376///
377/// Returns a [`ScannedBlock`] containing one [`WalletTx`] for each transaction in the
378/// block that is decryptable by any of the given keys. If an output is decrypted by a
379/// full viewing key, the nullifiers of that output will also be computed.
380///
381/// This is the second half of block scanning. Use [`decrypt_block`], or the
382/// `sync::decryptor` engine (available behind the `sync-decryptor` feature flag), for
383/// the first half.
384///
385/// # Panics
386///
387/// Panics if any of the [`BatchResult`]s in `vtx` references an `IvkTag` not present in
388/// `scanning_keys`. To uphold this precondition, use the same set of [`ScanningKeys`]
389/// with both this function and [`decrypt_block`] (or `sync::decryptor::new` if you are
390/// running the decryption engine yourself).
391///
392/// [`WalletTx`]: crate::wallet::WalletTx
393#[tracing::instrument(skip_all, fields(height = u32::from(height)))]
394#[allow(clippy::too_many_arguments)]
395pub fn scan_block<P, AccountId, IvkTag, E>(
396    params: &P,
397    height: BlockHeight,
398    header: &BlockHeader,
399    vtx: Vec<BatchResult<IvkTag>>,
400    scanning_keys: &ScanningKeys<AccountId, IvkTag>,
401    spend_ids: &SpendIdentifiers<AccountId>,
402    prior_block_metadata: Option<&BlockMetadata>,
403    #[cfg(feature = "transparent-inputs")] find_account_for_address: impl Fn(
404        &TransparentAddress,
405    ) -> Result<
406        Option<(AccountId, Option<TransparentKeyScope>)>,
407        E,
408    >,
409) -> Result<ScannedBlock<AccountId>, ScanBlockError<E>>
410where
411    P: consensus::Parameters + Send + 'static,
412    AccountId: Default + fmt::Debug + Ord + Hash + ConditionallySelectable + Send + Sync + 'static,
413    IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
414{
415    fn check_hash_continuity(
416        height: BlockHeight,
417        header: &BlockHeader,
418        prior_block_metadata: Option<&BlockMetadata>,
419    ) -> Option<ScanError> {
420        if let Some(prev) = prior_block_metadata {
421            if height != prev.block_height() + 1 {
422                debug!(
423                    "Block height discontinuity at {:?}, previous was {:?} ",
424                    height,
425                    prev.block_height()
426                );
427                return Some(ScanError::BlockHeightDiscontinuity {
428                    prev_height: prev.block_height(),
429                    new_height: height,
430                });
431            }
432
433            if header.prev_block != prev.block_hash() {
434                debug!("Block hash discontinuity at {:?}", height);
435                return Some(ScanError::PrevHashMismatch { at_height: height });
436            }
437        }
438
439        None
440    }
441
442    if let Some(scan_error) = check_hash_continuity(height, header, prior_block_metadata) {
443        return Err(scan_error.into());
444    }
445
446    trace!("Block continuity okay at {:?}", height);
447
448    let cur_hash = header.hash();
449    let zip212_enforcement = zip212_enforcement(params, height);
450
451    let mut pos_tracker = PositionTracker::for_block(params, height, &vtx, prior_block_metadata)?;
452
453    let mut wtxs: Vec<WalletTx<AccountId>> = vec![];
454
455    let mut sapling_nullifier_map = Vec::with_capacity(vtx.len());
456    let mut sapling_note_commitments: Vec<(sapling::Node, Retention<BlockHeight>)> = vec![];
457
458    #[cfg(feature = "orchard")]
459    let mut orchard_nullifier_map = Vec::with_capacity(vtx.len());
460    #[cfg(feature = "orchard")]
461    let mut orchard_note_commitments: Vec<(MerkleHashOrchard, Retention<BlockHeight>)> = vec![];
462
463    #[cfg(feature = "orchard")]
464    let mut ironwood_nullifier_map = Vec::with_capacity(vtx.len());
465    #[cfg(feature = "orchard")]
466    let mut ironwood_note_commitments: Vec<(MerkleHashOrchard, Retention<BlockHeight>)> = vec![];
467
468    #[cfg(feature = "transparent-inputs")]
469    let mut transparent_spend_map = Vec::with_capacity(vtx.len());
470
471    for (tx_index, batch) in vtx.into_iter().enumerate() {
472        let BatchResult {
473            tx,
474            sapling: sapling_decrypted,
475            #[cfg(feature = "orchard")]
476                orchard: orchard_decrypted,
477            #[cfg(feature = "orchard")]
478                ironwood: ironwood_decrypted,
479        } = batch;
480        let txid = tx.txid();
481        let tx_index =
482            TxIndex::try_from(tx_index).expect("Cannot fit more than 2^16 transactions in a block");
483
484        let (sapling_spends, sapling_unlinked_nullifiers) = tx
485            .sapling_bundle()
486            .map(|bundle| {
487                find_spent(
488                    bundle.shielded_spends(),
489                    spend_ids.shielded().sapling(),
490                    |spend| *spend.nullifier(),
491                    WalletSpend::from_parts,
492                )
493            })
494            .unwrap_or_default();
495
496        sapling_nullifier_map.push((tx_index, txid, sapling_unlinked_nullifiers));
497
498        #[cfg(feature = "orchard")]
499        let orchard_spends = {
500            let (orchard_spends, orchard_unlinked_nullifiers) = tx
501                .orchard_bundle()
502                .map(|bundle| {
503                    find_spent(
504                        bundle.actions().iter(),
505                        spend_ids.shielded().orchard(),
506                        |action| *action.nullifier(),
507                        WalletSpend::from_parts,
508                    )
509                })
510                .unwrap_or_default();
511            orchard_nullifier_map.push((tx_index, txid, orchard_unlinked_nullifiers));
512            orchard_spends
513        };
514
515        #[cfg(feature = "orchard")]
516        let ironwood_spends = {
517            let (ironwood_spends, ironwood_unlinked_nullifiers) = tx
518                .ironwood_bundle()
519                .map(|bundle| {
520                    find_spent(
521                        bundle.actions().iter(),
522                        spend_ids.shielded().ironwood(),
523                        |action| *action.nullifier(),
524                        WalletSpend::from_parts,
525                    )
526                })
527                .unwrap_or_default();
528            ironwood_nullifier_map.push((tx_index, txid, ironwood_unlinked_nullifiers));
529            ironwood_spends
530        };
531
532        // Detect spends of transparent outputs the wallet is tracking. A coinbase transaction's
533        // single input spends the null outpoint rather than a real prior output, so it is
534        // excluded: it can match no wallet output, and recording it would collide with the
535        // corresponding input of every other block's coinbase.
536        #[cfg(feature = "transparent-inputs")]
537        let (transparent_spends, unlinked_prevouts) = tx
538            .transparent_bundle()
539            .filter(|bundle| !bundle.is_coinbase())
540            .map(|bundle| {
541                find_transparent_spends(
542                    bundle.vin.iter().map(|txin| txin.prevout().clone()),
543                    spend_ids.transparent(),
544                )
545            })
546            .unwrap_or_default();
547
548        #[cfg(feature = "transparent-inputs")]
549        transparent_spend_map.push((tx_index, txid, unlinked_prevouts));
550
551        // Collect the set of accounts that were spent from in this transaction
552        let spent_from_accounts = sapling_spends.iter().map(|spend| spend.account_id());
553        #[cfg(feature = "transparent-inputs")]
554        let spent_from_accounts =
555            spent_from_accounts.chain(transparent_spends.iter().map(|spend| spend.account_id()));
556        #[cfg(feature = "orchard")]
557        let spent_from_accounts =
558            spent_from_accounts.chain(orchard_spends.iter().map(|spend| spend.account_id()));
559        #[cfg(feature = "orchard")]
560        let spent_from_accounts =
561            spent_from_accounts.chain(ironwood_spends.iter().map(|spend| spend.account_id()));
562        let spent_from_accounts = spent_from_accounts.copied().collect::<HashSet<_>>();
563
564        // TODO(#1305): Correctly track accounts that fund each transaction output. For now
565        // we pick a single funding account; when more than one wallet account contributed
566        // inputs we select the lowest account id, so that the choice is deterministic.
567        let funding_account = spent_from_accounts.iter().min().copied();
568        if spent_from_accounts.len() > 1 {
569            warn!(
570                "More than one wallet account detected as funding transaction {:?}, selecting {:?}",
571                txid,
572                funding_account
573                    .expect("funding_account is Some when spent_from_accounts is nonempty")
574            )
575        }
576
577        let transparent_outputs = detect_wallet_transparent_outputs(
578            params,
579            &tx,
580            Some(height),
581            funding_account,
582            #[cfg(feature = "transparent-inputs")]
583            &find_account_for_address,
584        )
585        .map_err(ScanBlockError::AddressLookup)?;
586
587        #[cfg(feature = "transparent-inputs")]
588        let has_transparent = !(transparent_spends.is_empty() && transparent_outputs.is_empty());
589        #[cfg(not(feature = "transparent-inputs"))]
590        let has_transparent = !transparent_outputs.is_empty();
591
592        let (sapling_outputs, mut sapling_nc) = tx
593            .sapling_bundle()
594            .map(|bundle| {
595                find_received(
596                    height,
597                    pos_tracker.tx_contains_last_sapling_outputs_in_block(&tx),
598                    txid,
599                    |output_idx| pos_tracker.sapling_note_position(output_idx),
600                    &scanning_keys.sapling,
601                    &spent_from_accounts,
602                    &bundle
603                        .shielded_outputs()
604                        .iter()
605                        .map(|output| (SaplingDomain::new(zip212_enforcement), output.clone()))
606                        .collect::<Vec<_>>(),
607                    Some(move |_| sapling_decrypted),
608                    batch::try_note_decryption,
609                    |output| sapling::Node::from_cmu(output.cmu()),
610                    |note| note,
611                )
612            })
613            .unwrap_or_default();
614        sapling_note_commitments.append(&mut sapling_nc);
615        let has_sapling = !(sapling_spends.is_empty() && sapling_outputs.is_empty());
616
617        #[cfg(feature = "orchard")]
618        let (orchard_outputs, mut orchard_nc) = tx
619            .orchard_bundle()
620            .map(|bundle| {
621                find_received(
622                    height,
623                    pos_tracker.tx_contains_last_orchard_actions_in_block(&tx),
624                    txid,
625                    |output_idx| pos_tracker.orchard_note_position(output_idx),
626                    &scanning_keys.orchard,
627                    &spent_from_accounts,
628                    &bundle
629                        .actions()
630                        .iter()
631                        .map(|action| (OrchardDomain::for_action(action), action.clone()))
632                        .collect::<Vec<_>>(),
633                    Some(move |_| orchard_decrypted),
634                    batch::try_note_decryption,
635                    |action| MerkleHashOrchard::from_cmx(action.cmx()),
636                    |note| (note, orchard::ValuePool::Orchard),
637                )
638            })
639            .unwrap_or_default();
640        #[cfg(feature = "orchard")]
641        orchard_note_commitments.append(&mut orchard_nc);
642
643        #[cfg(feature = "orchard")]
644        let (ironwood_outputs, mut ironwood_nc) = tx
645            .ironwood_bundle()
646            .map(|bundle| {
647                find_received(
648                    height,
649                    pos_tracker.tx_contains_last_ironwood_actions_in_block(&tx),
650                    txid,
651                    |output_idx| pos_tracker.ironwood_note_position(output_idx),
652                    &scanning_keys.ironwood,
653                    &spent_from_accounts,
654                    &bundle
655                        .actions()
656                        .iter()
657                        .map(|action| (IronwoodDomain::for_action(action), action.clone()))
658                        .collect::<Vec<_>>(),
659                    Some(move |_| ironwood_decrypted),
660                    batch::try_note_decryption,
661                    |action| MerkleHashOrchard::from_cmx(action.cmx()),
662                    |note| (note, orchard::ValuePool::Ironwood),
663                )
664            })
665            .unwrap_or_default();
666        #[cfg(feature = "orchard")]
667        ironwood_note_commitments.append(&mut ironwood_nc);
668
669        #[cfg(feature = "orchard")]
670        let has_orchard = !(orchard_spends.is_empty() && orchard_outputs.is_empty());
671        #[cfg(not(feature = "orchard"))]
672        let has_orchard = false;
673
674        #[cfg(feature = "orchard")]
675        let has_ironwood = !(ironwood_spends.is_empty() && ironwood_outputs.is_empty());
676        #[cfg(not(feature = "orchard"))]
677        let has_ironwood = false;
678
679        if has_transparent || has_sapling || has_orchard || has_ironwood {
680            wtxs.push(WalletTx::new(
681                txid,
682                tx_index,
683                #[cfg(feature = "transparent-inputs")]
684                transparent_spends,
685                #[cfg(not(feature = "transparent-inputs"))]
686                vec![],
687                transparent_outputs,
688                sapling_spends,
689                sapling_outputs,
690                #[cfg(feature = "orchard")]
691                orchard_spends,
692                #[cfg(feature = "orchard")]
693                orchard_outputs,
694                #[cfg(feature = "orchard")]
695                ironwood_spends,
696                #[cfg(feature = "orchard")]
697                ironwood_outputs,
698            ));
699        }
700
701        pos_tracker.increment_over_tx(&tx);
702    }
703
704    pos_tracker.check_end_of_block_consistency()?;
705
706    Ok(ScannedBlock::from_parts(
707        height,
708        cur_hash,
709        header.time,
710        wtxs,
711        ScannedBundles::new(
712            pos_tracker.sapling_final_tree_size,
713            sapling_note_commitments,
714            sapling_nullifier_map,
715        ),
716        #[cfg(feature = "orchard")]
717        ScannedBundles::new(
718            pos_tracker.orchard_final_tree_size,
719            orchard_note_commitments,
720            orchard_nullifier_map,
721        ),
722        #[cfg(feature = "orchard")]
723        ScannedBundles::new(
724            pos_tracker.ironwood_final_tree_size,
725            ironwood_note_commitments,
726            ironwood_nullifier_map,
727        ),
728        #[cfg(feature = "transparent-inputs")]
729        transparent_spend_map,
730    ))
731}
732
733/// Returns the size of the given shielded protocol's note commitment tree both before and
734/// after the application of a block at `at_height`.
735///
736/// - `activation_height` is the height at which `protocol` activated, if set.
737/// - `prior_tree_size` is the tree size as of the end of the previous block, if known.
738/// - `output_counts` yields the number of `protocol` outputs in each transaction of the
739///   block, in order.
740///
741/// Returns [`ScanError::TreeSizeUnknown`] if the starting size cannot be determined (the
742/// block is at or above the protocol's activation height but no prior size is known), or
743/// [`ScanError::TreeSizeOverflow`] if applying the block's outputs would take the tree
744/// size beyond the `u32` range.
745fn tree_sizes_around(
746    at_height: BlockHeight,
747    activation_height: Option<BlockHeight>,
748    prior_tree_size: Option<u32>,
749    mut output_counts: impl Iterator<Item = usize>,
750    protocol: ShieldedPool,
751) -> Result<(u32, u32), ScanError> {
752    let start_tree_size = match prior_tree_size {
753        Some(size) => size,
754        // If we're below the protocol's activation height, or it is not set, the tree
755        // size is zero; otherwise the starting size is unknown.
756        None => match activation_height {
757            Some(activation_height) if at_height >= activation_height => {
758                return Err(ScanError::TreeSizeUnknown {
759                    protocol,
760                    at_height,
761                });
762            }
763            _ => 0,
764        },
765    };
766
767    // We pre-compute the end tree size here so we can determine when we reach the last
768    // transaction in the block that adds notes to the tree. This enables us to correctly
769    // set the tree checkpoint in `find_received`. Note commitment tree sizes are
770    // `u32`-bounded by the protocol, so overflow here indicates corrupt or adversarial
771    // input rather than a valid chain state.
772    let overflow = || ScanError::TreeSizeOverflow {
773        protocol,
774        at_height,
775    };
776    let end_tree_size = output_counts.try_fold(start_tree_size, |acc, tx_outputs| {
777        let tx_outputs = u32::try_from(tx_outputs).map_err(|_| overflow())?;
778        acc.checked_add(tx_outputs).ok_or_else(overflow)
779    })?;
780
781    Ok((start_tree_size, end_tree_size))
782}
783
784/// Returns the number of Sapling outputs in `tx`.
785///
786/// Note commitment tree sizes are `u32`-bounded by the protocol, so a valid transaction
787/// can never contain more than `u32::MAX` outputs.
788fn sapling_output_count(tx: &Transaction) -> u32 {
789    tx.sapling_bundle().map_or(0, |b| {
790        u32::try_from(b.shielded_outputs().len()).expect("Sapling output count cannot exceed a u32")
791    })
792}
793
794/// Returns the number of Orchard actions in `tx`.
795///
796/// Note commitment tree sizes are `u32`-bounded by the protocol, so a valid transaction
797/// can never contain more than `u32::MAX` actions.
798#[cfg(feature = "orchard")]
799fn orchard_action_count(tx: &Transaction) -> u32 {
800    tx.orchard_bundle().map_or(0, |b| {
801        u32::try_from(b.actions().len()).expect("Orchard action count cannot exceed a u32")
802    })
803}
804
805/// Returns the number of Ironwood actions in `tx`.
806///
807/// Note commitment tree sizes are `u32`-bounded by the protocol, so a valid transaction
808/// can never contain more than `u32::MAX` actions.
809#[cfg(feature = "orchard")]
810fn ironwood_action_count(tx: &Transaction) -> u32 {
811    tx.ironwood_bundle().map_or(0, |b| {
812        u32::try_from(b.actions().len()).expect("Ironwood action count cannot exceed a u32")
813    })
814}
815
816impl PositionTracker {
817    fn for_block<P, IvkTag>(
818        params: &P,
819        at_height: BlockHeight,
820        vtx: &[BatchResult<IvkTag>],
821        prior_block_metadata: Option<&BlockMetadata>,
822    ) -> Result<Self, ScanError>
823    where
824        P: consensus::Parameters,
825    {
826        let (sapling_prior_tree_size, sapling_final_tree_size) = tree_sizes_around(
827            at_height,
828            params.activation_height(NetworkUpgrade::Sapling),
829            prior_block_metadata.and_then(|m| m.sapling_tree_size()),
830            vtx.iter().map(|b| {
831                b.tx.sapling_bundle()
832                    .map_or(0, |bd| bd.shielded_outputs().len())
833            }),
834            ShieldedPool::Sapling,
835        )?;
836
837        #[cfg(feature = "orchard")]
838        let (orchard_prior_tree_size, orchard_final_tree_size) = tree_sizes_around(
839            at_height,
840            params.activation_height(NetworkUpgrade::Nu5),
841            prior_block_metadata.and_then(|m| m.orchard_tree_size()),
842            vtx.iter()
843                .map(|b| b.tx.orchard_bundle().map_or(0, |bd| bd.actions().len())),
844            ShieldedPool::Orchard,
845        )?;
846
847        #[cfg(feature = "orchard")]
848        let (ironwood_prior_tree_size, ironwood_final_tree_size) = tree_sizes_around(
849            at_height,
850            params.activation_height(NetworkUpgrade::Nu6_3),
851            prior_block_metadata.and_then(|m| m.ironwood_tree_size()),
852            vtx.iter()
853                .map(|b| b.tx.ironwood_bundle().map_or(0, |bd| bd.actions().len())),
854            ShieldedPool::Ironwood,
855        )?;
856
857        Ok(Self {
858            sapling_tree_position: sapling_prior_tree_size,
859            sapling_final_tree_size,
860            #[cfg(feature = "orchard")]
861            orchard_tree_position: orchard_prior_tree_size,
862            #[cfg(feature = "orchard")]
863            orchard_final_tree_size,
864            #[cfg(feature = "orchard")]
865            ironwood_tree_position: ironwood_prior_tree_size,
866            #[cfg(feature = "orchard")]
867            ironwood_final_tree_size,
868        })
869    }
870
871    /// Returns `true` if a transaction contributing `sapling_output_count` outputs would
872    /// bring the Sapling tree position up to the block's final Sapling tree size; that is,
873    /// such a transaction contains the last Sapling output in the block.
874    fn contains_last_sapling_outputs(&self, sapling_output_count: u32) -> bool {
875        self.sapling_tree_position + sapling_output_count == self.sapling_final_tree_size
876    }
877
878    fn tx_contains_last_sapling_outputs_in_block(&self, tx: &Transaction) -> bool {
879        self.contains_last_sapling_outputs(sapling_output_count(tx))
880    }
881
882    /// Returns `true` if a transaction contributing `orchard_action_count` actions would
883    /// bring the Orchard tree position up to the block's final Orchard tree size; that is,
884    /// such a transaction contains the last Orchard output in the block.
885    #[cfg(feature = "orchard")]
886    fn contains_last_orchard_actions(&self, orchard_action_count: u32) -> bool {
887        self.orchard_tree_position + orchard_action_count == self.orchard_final_tree_size
888    }
889
890    #[cfg(feature = "orchard")]
891    fn tx_contains_last_orchard_actions_in_block(&self, tx: &Transaction) -> bool {
892        self.contains_last_orchard_actions(orchard_action_count(tx))
893    }
894
895    /// Returns `true` if a transaction contributing `ironwood_action_count` actions would
896    /// bring the Ironwood tree position up to the block's final Ironwood tree size; that is,
897    /// such a transaction contains the last Ironwood output in the block.
898    #[cfg(feature = "orchard")]
899    fn contains_last_ironwood_actions(&self, ironwood_action_count: u32) -> bool {
900        self.ironwood_tree_position + ironwood_action_count == self.ironwood_final_tree_size
901    }
902
903    #[cfg(feature = "orchard")]
904    fn tx_contains_last_ironwood_actions_in_block(&self, tx: &Transaction) -> bool {
905        self.contains_last_ironwood_actions(ironwood_action_count(tx))
906    }
907
908    /// Advances the tracked tree positions past a transaction with the given output
909    /// counts.
910    fn increment(
911        &mut self,
912        sapling_output_count: u32,
913        #[cfg(feature = "orchard")] orchard_action_count: u32,
914        #[cfg(feature = "orchard")] ironwood_action_count: u32,
915    ) {
916        self.sapling_tree_position += sapling_output_count;
917        #[cfg(feature = "orchard")]
918        {
919            self.orchard_tree_position += orchard_action_count;
920            self.ironwood_tree_position += ironwood_action_count;
921        }
922    }
923
924    fn increment_over_tx(&mut self, tx: &Transaction) {
925        self.increment(
926            sapling_output_count(tx),
927            #[cfg(feature = "orchard")]
928            orchard_action_count(tx),
929            #[cfg(feature = "orchard")]
930            ironwood_action_count(tx),
931        );
932    }
933
934    fn check_end_of_block_consistency(&self) -> Result<(), ScanError> {
935        // It is a programming error to construct `PositionTracker` from a `CompactBlock`
936        // and then not call `PositionTracker::increment_over_tx` on every transaction
937        // within the block.
938        assert_eq!(self.sapling_tree_position, self.sapling_final_tree_size);
939        #[cfg(feature = "orchard")]
940        assert_eq!(self.orchard_tree_position, self.orchard_final_tree_size);
941        #[cfg(feature = "orchard")]
942        assert_eq!(self.ironwood_tree_position, self.ironwood_final_tree_size);
943
944        Ok(())
945    }
946}
947
948#[cfg(test)]
949mod tests {
950    use proptest::prelude::*;
951
952    use zcash_protocol::{consensus::BlockHeight, testing::arb_protocol};
953
954    use super::{PositionTracker, tree_sizes_around};
955    use crate::scanning::ScanError;
956
957    // The behaviour of `tree_sizes_around` is independent of the shielded protocol (the
958    // protocol is only echoed back in errors), so these properties are checked for a
959    // `ShieldedPool` drawn uniformly at random via `arb_protocol`.
960    proptest! {
961        /// A known prior tree size is the starting point, and each transaction's outputs
962        /// are summed onto it to reach the final size. An empty block (no transactions, or
963        /// transactions with no outputs) leaves the size unchanged.
964        #[test]
965        fn uses_known_prior_size(
966            protocol in arb_protocol(),
967            at_height in 0u32..1_000_000,
968            prior in 0u32..100_000,
969            counts in prop::collection::vec(0usize..1_000, 0..16),
970        ) {
971            let expected_end = prior + counts.iter().map(|c| *c as u32).sum::<u32>();
972            let result = tree_sizes_around(
973                BlockHeight::from(at_height),
974                Some(BlockHeight::from(1u32)),
975                Some(prior),
976                counts.into_iter(),
977                protocol,
978            );
979            prop_assert_eq!(result.unwrap(), (prior, expected_end));
980        }
981
982        /// With no prior size and a block below the activation height, the tree starts
983        /// empty.
984        #[test]
985        fn zero_below_activation_without_prior(
986            protocol in arb_protocol(),
987            at_height in 0u32..1_000_000,
988            activation_offset in 1u32..1_000,
989            counts in prop::collection::vec(0usize..1_000, 0..16),
990        ) {
991            // `at_height` is strictly below the activation height.
992            let activation = at_height + activation_offset;
993            let expected_end: u32 = counts.iter().map(|c| *c as u32).sum();
994            let result = tree_sizes_around(
995                BlockHeight::from(at_height),
996                Some(BlockHeight::from(activation)),
997                None,
998                counts.into_iter(),
999                protocol,
1000            );
1001            prop_assert_eq!(result.unwrap(), (0, expected_end));
1002        }
1003
1004        /// With no prior size and no activation height (e.g. a protocol not activated on
1005        /// this network), the tree starts empty regardless of the block height.
1006        #[test]
1007        fn zero_when_activation_unset(
1008            protocol in arb_protocol(),
1009            at_height in 0u32..1_000_000,
1010            counts in prop::collection::vec(0usize..1_000, 0..16),
1011        ) {
1012            let expected_end: u32 = counts.iter().map(|c| *c as u32).sum();
1013            let result = tree_sizes_around(
1014                BlockHeight::from(at_height),
1015                None,
1016                None,
1017                counts.into_iter(),
1018                protocol,
1019            );
1020            prop_assert_eq!(result.unwrap(), (0, expected_end));
1021        }
1022
1023        /// A known prior size is used even when the block is below the activation height.
1024        #[test]
1025        fn prior_size_takes_precedence_below_activation(
1026            protocol in arb_protocol(),
1027            at_height in 0u32..1_000_000,
1028            activation_offset in 1u32..1_000,
1029            prior in 0u32..100_000,
1030            counts in prop::collection::vec(0usize..1_000, 0..16),
1031        ) {
1032            let activation = at_height + activation_offset;
1033            let expected_end = prior + counts.iter().map(|c| *c as u32).sum::<u32>();
1034            let result = tree_sizes_around(
1035                BlockHeight::from(at_height),
1036                Some(BlockHeight::from(activation)),
1037                Some(prior),
1038                counts.into_iter(),
1039                protocol,
1040            );
1041            prop_assert_eq!(result.unwrap(), (prior, expected_end));
1042        }
1043
1044        /// At or above the activation height with no prior size, the starting size cannot
1045        /// be determined, and the error reports the queried protocol and height.
1046        #[test]
1047        fn unknown_at_or_above_activation_without_prior(
1048            protocol in arb_protocol(),
1049            activation in 0u32..1_000_000,
1050            delta in 0u32..1_000,
1051        ) {
1052            // `at_height` is at or above the activation height.
1053            let at_height = activation + delta;
1054            let result = tree_sizes_around(
1055                BlockHeight::from(at_height),
1056                Some(BlockHeight::from(activation)),
1057                None,
1058                std::iter::empty(),
1059                protocol,
1060            );
1061            let matched = matches!(
1062                result,
1063                Err(ScanError::TreeSizeUnknown { protocol: p, at_height: h })
1064                    if p == protocol && h == BlockHeight::from(at_height)
1065            );
1066            prop_assert!(matched);
1067        }
1068
1069        /// Applying the block's outputs would push the tree size beyond the `u32` range,
1070        /// and the error reports the queried protocol.
1071        #[test]
1072        fn overflow_is_reported(
1073            protocol in arb_protocol(),
1074            headroom in 0u32..100,
1075            extra in 1u32..100,
1076        ) {
1077            // A single transaction whose output count exceeds the remaining headroom takes
1078            // the tree size past `u32::MAX`.
1079            let prior = u32::MAX - headroom;
1080            let count = headroom as usize + extra as usize;
1081            let result = tree_sizes_around(
1082                BlockHeight::from(100u32),
1083                Some(BlockHeight::from(1u32)),
1084                Some(prior),
1085                std::iter::once(count),
1086                protocol,
1087            );
1088            let matched = matches!(
1089                result,
1090                Err(ScanError::TreeSizeOverflow { protocol: p, .. }) if p == protocol
1091            );
1092            prop_assert!(matched);
1093        }
1094
1095        /// Reaching exactly `u32::MAX` is a valid (non-overflowing) final size.
1096        #[test]
1097        fn exact_u32_max_boundary_is_not_overflow(
1098            protocol in arb_protocol(),
1099            count in 0u32..1_000,
1100        ) {
1101            let prior = u32::MAX - count;
1102            let result = tree_sizes_around(
1103                BlockHeight::from(100u32),
1104                Some(BlockHeight::from(1u32)),
1105                Some(prior),
1106                std::iter::once(count as usize),
1107                protocol,
1108            );
1109            prop_assert_eq!(result.unwrap(), (prior, u32::MAX));
1110        }
1111    }
1112
1113    #[test]
1114    fn contains_last_sapling_outputs_detects_boundary() {
1115        let tracker = PositionTracker {
1116            sapling_tree_position: 10,
1117            sapling_final_tree_size: 15,
1118            #[cfg(feature = "orchard")]
1119            orchard_tree_position: 0,
1120            #[cfg(feature = "orchard")]
1121            orchard_final_tree_size: 0,
1122            #[cfg(feature = "orchard")]
1123            ironwood_tree_position: 0,
1124            #[cfg(feature = "orchard")]
1125            ironwood_final_tree_size: 0,
1126        };
1127
1128        // A transaction adding exactly the remaining outputs contains the block's last
1129        // Sapling output; any other count does not land on the boundary.
1130        assert!(tracker.contains_last_sapling_outputs(5));
1131        assert!(!tracker.contains_last_sapling_outputs(0));
1132        assert!(!tracker.contains_last_sapling_outputs(4));
1133        assert!(!tracker.contains_last_sapling_outputs(6));
1134    }
1135
1136    #[test]
1137    fn increment_advances_sapling_position_to_final_size() {
1138        let mut tracker = PositionTracker {
1139            sapling_tree_position: 0,
1140            sapling_final_tree_size: 6,
1141            #[cfg(feature = "orchard")]
1142            orchard_tree_position: 0,
1143            #[cfg(feature = "orchard")]
1144            orchard_final_tree_size: 0,
1145            #[cfg(feature = "orchard")]
1146            ironwood_tree_position: 0,
1147            #[cfg(feature = "orchard")]
1148            ironwood_final_tree_size: 0,
1149        };
1150
1151        // Walk a block of three transactions with 2, 0 and 4 Sapling outputs; only the
1152        // last one lands on the boundary.
1153        assert!(!tracker.contains_last_sapling_outputs(2));
1154        tracker.increment(
1155            2,
1156            #[cfg(feature = "orchard")]
1157            0,
1158            #[cfg(feature = "orchard")]
1159            0,
1160        );
1161        assert!(!tracker.contains_last_sapling_outputs(0));
1162        tracker.increment(
1163            0,
1164            #[cfg(feature = "orchard")]
1165            0,
1166            #[cfg(feature = "orchard")]
1167            0,
1168        );
1169        assert!(tracker.contains_last_sapling_outputs(4));
1170        tracker.increment(
1171            4,
1172            #[cfg(feature = "orchard")]
1173            0,
1174            #[cfg(feature = "orchard")]
1175            0,
1176        );
1177
1178        assert_eq!(tracker.sapling_tree_position, 6);
1179        tracker.check_end_of_block_consistency().unwrap();
1180    }
1181
1182    #[cfg(feature = "orchard")]
1183    #[test]
1184    fn increment_advances_orchard_position_to_final_size() {
1185        let mut tracker = PositionTracker {
1186            sapling_tree_position: 0,
1187            sapling_final_tree_size: 0,
1188            orchard_tree_position: 4,
1189            orchard_final_tree_size: 9,
1190            ironwood_tree_position: 0,
1191            ironwood_final_tree_size: 0,
1192        };
1193
1194        assert!(tracker.contains_last_orchard_actions(5));
1195        assert!(!tracker.contains_last_orchard_actions(4));
1196        tracker.increment(0, 5, 0);
1197
1198        assert_eq!(tracker.orchard_tree_position, 9);
1199        tracker.check_end_of_block_consistency().unwrap();
1200    }
1201
1202    #[test]
1203    #[should_panic]
1204    fn check_end_of_block_consistency_panics_when_incomplete() {
1205        // Failing to increment over every transaction in the block is a programming error,
1206        // surfaced as a panic.
1207        let tracker = PositionTracker {
1208            sapling_tree_position: 3,
1209            sapling_final_tree_size: 6,
1210            #[cfg(feature = "orchard")]
1211            orchard_tree_position: 0,
1212            #[cfg(feature = "orchard")]
1213            orchard_final_tree_size: 0,
1214            #[cfg(feature = "orchard")]
1215            ironwood_tree_position: 0,
1216            #[cfg(feature = "orchard")]
1217            ironwood_final_tree_size: 0,
1218        };
1219        let _ = tracker.check_end_of_block_consistency();
1220    }
1221}