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
50pub(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#[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 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 #[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 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
250pub(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 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 #[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
313pub 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
329pub 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 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#[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 #[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 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 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
733fn 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 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 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
784fn 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#[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#[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 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 #[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 #[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 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 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 proptest! {
961 #[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 #[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 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 #[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 #[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 #[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 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 #[test]
1072 fn overflow_is_reported(
1073 protocol in arb_protocol(),
1074 headroom in 0u32..100,
1075 extra in 1u32..100,
1076 ) {
1077 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 #[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 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 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 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}