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