1use ScanError::*;
5
6use core::{
7 convert::TryFrom,
8 fmt::{self, Debug},
9};
10use std::{
11 collections::{HashMap, HashSet},
12 hash::Hash,
13};
14
15use incrementalmerkletree::{Marking, Position, Retention};
16use sapling::{SaplingIvk, note_encryption::SaplingDomain};
17use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption};
18
19use zcash_keys::keys::UnifiedFullViewingKey;
20use zcash_note_encryption::{BatchDomain, Domain, ShieldedOutput};
21use zcash_primitives::transaction::TxId;
22use zcash_protocol::{
23 ShieldedPool,
24 consensus::{self, BlockHeight},
25};
26use zip32::Scope;
27
28use crate::{
29 data_api::{BlockMetadata, NullifierQuery, ScannedBlock, WalletRead},
30 proto::compact_formats::CompactBlock,
31 scan::DecryptedOutput,
32 wallet::WalletOutput,
33};
34
35#[cfg(feature = "transparent-inputs")]
36use crate::wallet::WalletTransparentSpend;
37
38#[cfg(feature = "orchard")]
39use orchard::note_encryption::OrchardDomain;
40
41#[cfg(feature = "transparent-inputs")]
42use transparent::{address::TransparentAddress, bundle::OutPoint, keys::TransparentKeyScope};
43
44pub(crate) mod compact;
45pub mod full;
46
47pub trait ScanningKeyOps<D: Domain, AccountId, Nf> {
62 fn prepare(&self) -> D::IncomingViewingKey;
64
65 fn account_id(&self) -> &AccountId;
70
71 fn key_scope(&self) -> Option<Scope>;
73
74 fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf>;
79}
80
81impl<D: Domain, AccountId, Nf, K: ScanningKeyOps<D, AccountId, Nf>> ScanningKeyOps<D, AccountId, Nf>
82 for &K
83{
84 fn prepare(&self) -> D::IncomingViewingKey {
85 (*self).prepare()
86 }
87
88 fn account_id(&self) -> &AccountId {
89 (*self).account_id()
90 }
91
92 fn key_scope(&self) -> Option<Scope> {
93 (*self).key_scope()
94 }
95
96 fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
97 (*self).nf(note, note_position)
98 }
99}
100
101impl<D: Domain, AccountId, Nf> ScanningKeyOps<D, AccountId, Nf>
102 for Box<dyn ScanningKeyOps<D, AccountId, Nf>>
103{
104 fn prepare(&self) -> D::IncomingViewingKey {
105 self.as_ref().prepare()
106 }
107
108 fn account_id(&self) -> &AccountId {
109 self.as_ref().account_id()
110 }
111
112 fn key_scope(&self) -> Option<Scope> {
113 self.as_ref().key_scope()
114 }
115
116 fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
117 self.as_ref().nf(note, note_position)
118 }
119}
120
121impl<D: Domain, AccountId: Send + Sync, Nf> ScanningKeyOps<D, AccountId, Nf>
122 for Box<dyn ScanningKeyOps<D, AccountId, Nf> + Send + Sync>
123{
124 fn prepare(&self) -> D::IncomingViewingKey {
125 self.as_ref().prepare()
126 }
127
128 fn account_id(&self) -> &AccountId {
129 self.as_ref().account_id()
130 }
131
132 fn key_scope(&self) -> Option<Scope> {
133 self.as_ref().key_scope()
134 }
135
136 fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
137 self.as_ref().nf(note, note_position)
138 }
139}
140
141pub struct ScanningKey<Ivk, Nk, AccountId> {
143 ivk: Ivk,
144 nk: Option<Nk>,
145 account_id: AccountId,
146 key_scope: Option<Scope>,
147}
148
149impl<Ivk, Nk, AccountId> ScanningKey<Ivk, Nk, AccountId> {
150 pub fn new(ivk: Ivk, nk: Option<Nk>, account_id: AccountId, key_scope: Option<Scope>) -> Self {
152 Self {
153 ivk,
154 nk,
155 account_id,
156 key_scope,
157 }
158 }
159}
160
161impl<AccountId> ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>
162 for ScanningKey<sapling::SaplingIvk, sapling::NullifierDerivingKey, AccountId>
163{
164 fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey {
165 sapling::note_encryption::PreparedIncomingViewingKey::new(&self.ivk)
166 }
167
168 fn nf(&self, note: &sapling::Note, position: Position) -> Option<sapling::Nullifier> {
169 self.nk.as_ref().map(|key| note.nf(key, position.into()))
170 }
171
172 fn account_id(&self) -> &AccountId {
173 &self.account_id
174 }
175
176 fn key_scope(&self) -> Option<Scope> {
177 self.key_scope
178 }
179}
180
181impl<AccountId> ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>
182 for (AccountId, SaplingIvk)
183{
184 fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey {
185 sapling::note_encryption::PreparedIncomingViewingKey::new(&self.1)
186 }
187
188 fn nf(&self, _note: &sapling::Note, _position: Position) -> Option<sapling::Nullifier> {
189 None
190 }
191
192 fn account_id(&self) -> &AccountId {
193 &self.0
194 }
195
196 fn key_scope(&self) -> Option<Scope> {
197 None
198 }
199}
200
201#[cfg(feature = "orchard")]
202impl<AccountId> ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>
203 for ScanningKey<orchard::keys::IncomingViewingKey, orchard::keys::FullViewingKey, AccountId>
204{
205 fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey {
206 orchard::keys::PreparedIncomingViewingKey::new(&self.ivk)
207 }
208
209 fn nf(
210 &self,
211 note: &orchard::note::Note,
212 _position: Position,
213 ) -> Option<orchard::note::Nullifier> {
214 self.nk.as_ref().map(|key| note.nullifier(key))
215 }
216
217 fn account_id(&self) -> &AccountId {
218 &self.account_id
219 }
220
221 fn key_scope(&self) -> Option<Scope> {
222 self.key_scope
223 }
224}
225
226#[cfg(feature = "orchard")]
236pub(crate) type IronwoodDomain = orchard::note_encryption::IronwoodDomain;
237
238#[cfg(feature = "orchard")]
241pub(crate) type IronwoodNullifier = orchard::note::Nullifier;
242
243#[cfg(feature = "orchard")]
247impl<AccountId> ScanningKeyOps<IronwoodDomain, AccountId, orchard::note::Nullifier>
248 for ScanningKey<orchard::keys::IncomingViewingKey, orchard::keys::FullViewingKey, AccountId>
249{
250 fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey {
251 orchard::keys::PreparedIncomingViewingKey::new(&self.ivk)
252 }
253
254 fn nf(
255 &self,
256 note: &orchard::note::Note,
257 _position: Position,
258 ) -> Option<orchard::note::Nullifier> {
259 self.nk.as_ref().map(|key| note.nullifier(key))
260 }
261
262 fn account_id(&self) -> &AccountId {
263 &self.account_id
264 }
265
266 fn key_scope(&self) -> Option<Scope> {
267 self.key_scope
268 }
269}
270
271pub struct ScanningKeys<AccountId, IvkTag> {
273 sapling: HashMap<
274 IvkTag,
275 Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
276 >,
277 #[cfg(feature = "orchard")]
278 orchard: HashMap<
279 IvkTag,
280 Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier> + Send + Sync>,
281 >,
282 #[cfg(feature = "orchard")]
283 ironwood: HashMap<
284 IvkTag,
285 Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
286 >,
287}
288
289impl<AccountId, IvkTag> ScanningKeys<AccountId, IvkTag> {
290 pub fn new(
292 sapling: HashMap<
293 IvkTag,
294 Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
295 >,
296 #[cfg(feature = "orchard")] orchard: HashMap<
297 IvkTag,
298 Box<
299 dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>
300 + Send
301 + Sync,
302 >,
303 >,
304 #[cfg(feature = "orchard")] ironwood: HashMap<
305 IvkTag,
306 Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
307 >,
308 ) -> Self {
309 Self {
310 sapling,
311 #[cfg(feature = "orchard")]
312 orchard,
313 #[cfg(feature = "orchard")]
314 ironwood,
315 }
316 }
317
318 pub fn empty() -> Self {
320 Self {
321 sapling: HashMap::new(),
322 #[cfg(feature = "orchard")]
323 orchard: HashMap::new(),
324 #[cfg(feature = "orchard")]
325 ironwood: HashMap::new(),
326 }
327 }
328
329 pub fn sapling(
331 &self,
332 ) -> &HashMap<
333 IvkTag,
334 Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
335 > {
336 &self.sapling
337 }
338
339 #[cfg(feature = "orchard")]
341 pub fn orchard(
342 &self,
343 ) -> &HashMap<
344 IvkTag,
345 Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier> + Send + Sync>,
346 > {
347 &self.orchard
348 }
349
350 #[cfg(feature = "orchard")]
356 pub fn ironwood(
357 &self,
358 ) -> &HashMap<
359 IvkTag,
360 Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
361 > {
362 &self.ironwood
363 }
364}
365
366impl<AccountId: Copy + Eq + Hash + Send + Sync + 'static>
367 ScanningKeys<AccountId, (AccountId, Scope)>
368{
369 pub fn from_account_ufvks(
372 ufvks: impl IntoIterator<Item = (AccountId, UnifiedFullViewingKey)>,
373 ) -> Self {
374 #![allow(clippy::type_complexity)]
375
376 let mut sapling: HashMap<
377 (AccountId, Scope),
378 Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
379 > = HashMap::new();
380 #[cfg(feature = "orchard")]
381 let mut orchard: HashMap<
382 (AccountId, Scope),
383 Box<
384 dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>
385 + Send
386 + Sync,
387 >,
388 > = HashMap::new();
389 #[cfg(feature = "orchard")]
390 let mut ironwood: HashMap<
391 (AccountId, Scope),
392 Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
393 > = HashMap::new();
394
395 for (account_id, ufvk) in ufvks {
396 if let Some(dfvk) = ufvk.sapling() {
397 for scope in [Scope::External, Scope::Internal] {
398 sapling.insert(
399 (account_id, scope),
400 Box::new(ScanningKey {
401 ivk: dfvk.to_ivk(scope),
402 nk: Some(dfvk.to_nk(scope)),
403 account_id,
404 key_scope: Some(scope),
405 }),
406 );
407 }
408 }
409
410 #[cfg(feature = "orchard")]
411 if let Some(fvk) = ufvk.orchard() {
412 for scope in [Scope::External, Scope::Internal] {
413 orchard.insert(
414 (account_id, scope),
415 Box::new(ScanningKey {
416 ivk: fvk.to_ivk(scope),
417 nk: Some(fvk.clone()),
418 account_id,
419 key_scope: Some(scope),
420 }),
421 );
422 }
423
424 for scope in [Scope::External, Scope::Internal] {
427 ironwood.insert(
428 (account_id, scope),
429 Box::new(ScanningKey {
430 ivk: fvk.to_ivk(scope),
431 nk: Some(fvk.clone()),
432 account_id,
433 key_scope: Some(scope),
434 }),
435 );
436 }
437 }
438 }
439
440 Self {
441 sapling,
442 #[cfg(feature = "orchard")]
443 orchard,
444 #[cfg(feature = "orchard")]
445 ironwood,
446 }
447 }
448}
449
450pub struct Nullifiers<AccountId> {
458 sapling: Vec<(AccountId, sapling::Nullifier)>,
459 #[cfg(feature = "orchard")]
460 orchard: Vec<(AccountId, orchard::note::Nullifier)>,
461 #[cfg(feature = "orchard")]
462 ironwood: Vec<(AccountId, IronwoodNullifier)>,
463}
464
465impl<AccountId> Nullifiers<AccountId> {
466 pub fn empty() -> Self {
468 Self {
469 sapling: vec![],
470 #[cfg(feature = "orchard")]
471 orchard: vec![],
472 #[cfg(feature = "orchard")]
473 ironwood: vec![],
474 }
475 }
476
477 pub fn unspent<DbT: WalletRead<AccountId = AccountId>>(
479 db_data: &DbT,
480 ) -> Result<Self, DbT::Error> {
481 Ok(Self::new(
482 db_data.get_sapling_nullifiers(NullifierQuery::Unspent)?,
483 #[cfg(feature = "orchard")]
484 db_data.get_orchard_nullifiers(NullifierQuery::Unspent)?,
485 #[cfg(feature = "orchard")]
486 db_data.get_ironwood_nullifiers(NullifierQuery::Unspent)?,
487 ))
488 }
489
490 pub(crate) fn new(
492 sapling: Vec<(AccountId, sapling::Nullifier)>,
493 #[cfg(feature = "orchard")] orchard: Vec<(AccountId, orchard::note::Nullifier)>,
494 #[cfg(feature = "orchard")] ironwood: Vec<(AccountId, IronwoodNullifier)>,
495 ) -> Self {
496 Self {
497 sapling,
498 #[cfg(feature = "orchard")]
499 orchard,
500 #[cfg(feature = "orchard")]
501 ironwood,
502 }
503 }
504
505 pub fn sapling(&self) -> &[(AccountId, sapling::Nullifier)] {
507 self.sapling.as_ref()
508 }
509
510 #[cfg(feature = "orchard")]
512 pub fn orchard(&self) -> &[(AccountId, orchard::note::Nullifier)] {
513 self.orchard.as_ref()
514 }
515
516 #[cfg(feature = "orchard")]
518 pub fn ironwood(&self) -> &[(AccountId, IronwoodNullifier)] {
519 self.ironwood.as_ref()
520 }
521
522 pub(crate) fn retain_sapling(&mut self, f: impl Fn(&(AccountId, sapling::Nullifier)) -> bool) {
525 self.sapling.retain(f);
526 }
527
528 pub(crate) fn extend_sapling(
530 &mut self,
531 nfs: impl IntoIterator<Item = (AccountId, sapling::Nullifier)>,
532 ) {
533 self.sapling.extend(nfs);
534 }
535
536 #[cfg(feature = "orchard")]
537 pub(crate) fn retain_orchard(
538 &mut self,
539 f: impl Fn(&(AccountId, orchard::note::Nullifier)) -> bool,
540 ) {
541 self.orchard.retain(f);
542 }
543
544 #[cfg(feature = "orchard")]
545 pub(crate) fn extend_orchard(
546 &mut self,
547 nfs: impl IntoIterator<Item = (AccountId, orchard::note::Nullifier)>,
548 ) {
549 self.orchard.extend(nfs);
550 }
551
552 #[cfg(feature = "orchard")]
553 pub(crate) fn retain_ironwood(&mut self, f: impl Fn(&(AccountId, IronwoodNullifier)) -> bool) {
554 self.ironwood.retain(f);
555 }
556
557 #[cfg(feature = "orchard")]
558 pub(crate) fn extend_ironwood(
559 &mut self,
560 nfs: impl IntoIterator<Item = (AccountId, IronwoodNullifier)>,
561 ) {
562 self.ironwood.extend(nfs);
563 }
564}
565
566impl<AccountId: Copy> Nullifiers<AccountId> {
567 pub fn update_with(&mut self, scanned_block: &ScannedBlock<AccountId>) {
578 let sapling_spent_nf: Vec<&sapling::Nullifier> = scanned_block
579 .transactions()
580 .iter()
581 .flat_map(|tx| tx.sapling_spends().iter().map(|spend| spend.nf()))
582 .collect();
583
584 self.retain_sapling(|(_, nf)| !sapling_spent_nf.contains(&nf));
585 self.extend_sapling(scanned_block.transactions().iter().flat_map(|tx| {
586 tx.sapling_outputs()
587 .iter()
588 .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
589 }));
590
591 #[cfg(feature = "orchard")]
592 {
593 let orchard_spent_nf: Vec<&orchard::note::Nullifier> = scanned_block
594 .transactions()
595 .iter()
596 .flat_map(|tx| tx.orchard_spends().iter().map(|spend| spend.nf()))
597 .collect();
598
599 self.retain_orchard(|(_, nf)| !orchard_spent_nf.contains(&nf));
600 self.extend_orchard(scanned_block.transactions().iter().flat_map(|tx| {
601 tx.orchard_outputs()
602 .iter()
603 .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
604 }));
605
606 let ironwood_spent_nf: Vec<&IronwoodNullifier> = scanned_block
607 .transactions()
608 .iter()
609 .flat_map(|tx| tx.ironwood_spends().iter().map(|spend| spend.nf()))
610 .collect();
611
612 self.retain_ironwood(|(_, nf)| !ironwood_spent_nf.contains(&nf));
613 self.extend_ironwood(scanned_block.transactions().iter().flat_map(|tx| {
614 tx.ironwood_outputs()
615 .iter()
616 .flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
617 }));
618 }
619 }
620}
621
622pub struct SpendIdentifiers<AccountId> {
639 shielded: Nullifiers<AccountId>,
640 #[cfg(feature = "transparent-inputs")]
641 transparent: HashMap<OutPoint, AccountId>,
642}
643
644impl<AccountId> SpendIdentifiers<AccountId> {
645 pub fn empty() -> Self {
647 Self {
648 shielded: Nullifiers::empty(),
649 #[cfg(feature = "transparent-inputs")]
650 transparent: HashMap::new(),
651 }
652 }
653
654 pub fn unspent<DbT: WalletRead<AccountId = AccountId>>(
656 db_data: &DbT,
657 ) -> Result<Self, DbT::Error> {
658 Ok(Self::new(
659 Nullifiers::unspent(db_data)?,
660 #[cfg(feature = "transparent-inputs")]
661 db_data.get_unspent_transparent_outpoints()?,
662 ))
663 }
664
665 pub(crate) fn new(
667 shielded: Nullifiers<AccountId>,
668 #[cfg(feature = "transparent-inputs")] transparent: HashMap<OutPoint, AccountId>,
669 ) -> Self {
670 Self {
671 shielded,
672 #[cfg(feature = "transparent-inputs")]
673 transparent,
674 }
675 }
676
677 pub fn shielded(&self) -> &Nullifiers<AccountId> {
679 &self.shielded
680 }
681
682 #[cfg(feature = "transparent-inputs")]
685 pub fn transparent(&self) -> &HashMap<OutPoint, AccountId> {
686 &self.transparent
687 }
688}
689
690impl<AccountId: Copy> SpendIdentifiers<AccountId> {
691 pub fn update_with(&mut self, scanned_block: &ScannedBlock<AccountId>) {
698 self.shielded.update_with(scanned_block);
699
700 #[cfg(feature = "transparent-inputs")]
701 {
702 for spend in scanned_block
703 .transactions()
704 .iter()
705 .flat_map(|tx| tx.transparent_spends())
706 {
707 self.transparent.remove(spend.outpoint());
708 }
709
710 self.transparent.extend(
711 scanned_block
712 .transactions()
713 .iter()
714 .flat_map(|tx| tx.transparent_outputs())
715 .filter_map(|out| {
716 out.recipient_account()
717 .map(|account_id| (out.outpoint().clone(), *account_id))
718 }),
719 );
720 }
721 }
722}
723
724#[derive(Clone, Debug)]
726#[non_exhaustive]
727pub enum ScanError {
728 EncodingInvalid {
730 at_height: BlockHeight,
731 txid: TxId,
732 pool_type: ShieldedPool,
733 index: usize,
734 },
735
736 PrevHashMismatch { at_height: BlockHeight },
739
740 BlockHeightDiscontinuity {
743 prev_height: BlockHeight,
744 new_height: BlockHeight,
745 },
746
747 TreeSizeMismatch {
750 protocol: ShieldedPool,
751 at_height: BlockHeight,
752 given: u32,
753 computed: u32,
754 },
755
756 TreeSizeUnknown {
760 protocol: ShieldedPool,
761 at_height: BlockHeight,
762 },
763
764 TreeSizeInvalid {
768 protocol: ShieldedPool,
769 at_height: BlockHeight,
770 },
771
772 TreeSizeOverflow {
775 protocol: ShieldedPool,
776 at_height: BlockHeight,
777 },
778
779 #[cfg(feature = "transparent-inputs")]
785 TransparentPrevoutInvalid {
786 at_height: BlockHeight,
787 txid: TxId,
788 index: usize,
789 },
790}
791
792impl ScanError {
793 pub fn is_continuity_error(&self) -> bool {
795 match self {
796 EncodingInvalid { .. } => false,
797 PrevHashMismatch { .. } => true,
798 BlockHeightDiscontinuity { .. } => true,
799 TreeSizeMismatch { .. } => true,
800 TreeSizeUnknown { .. } => false,
801 TreeSizeInvalid { .. } => false,
802 TreeSizeOverflow { .. } => false,
803 #[cfg(feature = "transparent-inputs")]
804 TransparentPrevoutInvalid { .. } => false,
805 }
806 }
807
808 pub fn at_height(&self) -> BlockHeight {
810 match self {
811 EncodingInvalid { at_height, .. } => *at_height,
812 PrevHashMismatch { at_height } => *at_height,
813 BlockHeightDiscontinuity { new_height, .. } => *new_height,
814 TreeSizeMismatch { at_height, .. } => *at_height,
815 TreeSizeUnknown { at_height, .. } => *at_height,
816 TreeSizeInvalid { at_height, .. } => *at_height,
817 TreeSizeOverflow { at_height, .. } => *at_height,
818 #[cfg(feature = "transparent-inputs")]
819 TransparentPrevoutInvalid { at_height, .. } => *at_height,
820 }
821 }
822}
823
824impl fmt::Display for ScanError {
825 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
826 match &self {
827 EncodingInvalid {
828 txid,
829 pool_type,
830 index,
831 ..
832 } => write!(
833 f,
834 "{pool_type:?} output {index} of transaction {txid} was improperly encoded."
835 ),
836 PrevHashMismatch { at_height } => write!(
837 f,
838 "The parent hash of proposed block does not correspond to the block hash at height {at_height}."
839 ),
840 BlockHeightDiscontinuity {
841 prev_height,
842 new_height,
843 } => {
844 write!(
845 f,
846 "Block height discontinuity at height {new_height}; previous height was: {prev_height}"
847 )
848 }
849 TreeSizeMismatch {
850 protocol,
851 at_height,
852 given,
853 computed,
854 } => {
855 write!(
856 f,
857 "The {protocol:?} note commitment tree size provided by a compact block did not match the expected size at height {at_height}; given {given}, expected {computed}"
858 )
859 }
860 TreeSizeUnknown {
861 protocol,
862 at_height,
863 } => {
864 write!(
865 f,
866 "Unable to determine {protocol:?} note commitment tree size at height {at_height}"
867 )
868 }
869 TreeSizeInvalid {
870 protocol,
871 at_height,
872 } => {
873 write!(
874 f,
875 "Received invalid (potentially default) {protocol:?} note commitment tree size metadata at height {at_height}"
876 )
877 }
878 TreeSizeOverflow {
879 protocol,
880 at_height,
881 } => {
882 write!(
883 f,
884 "The {protocol:?} note commitment tree size at height {at_height} would exceed the `u32` range."
885 )
886 }
887 #[cfg(feature = "transparent-inputs")]
888 TransparentPrevoutInvalid {
889 txid,
890 index,
891 at_height,
892 } => {
893 write!(
894 f,
895 "The prevout of transparent input {index} of transaction {txid} at height {at_height} was improperly encoded."
896 )
897 }
898 }
899 }
900}
901
902impl core::error::Error for ScanError {}
903
904#[derive(Debug)]
907#[non_exhaustive]
908pub enum ScanBlockError<E> {
909 Scan(ScanError),
911 AddressLookup(E),
914}
915
916impl<E> From<ScanError> for ScanBlockError<E> {
917 fn from(e: ScanError) -> Self {
918 ScanBlockError::Scan(e)
919 }
920}
921
922impl<E: fmt::Display> fmt::Display for ScanBlockError<E> {
923 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
924 match self {
925 ScanBlockError::Scan(e) => write!(f, "Error scanning block: {e}"),
926 ScanBlockError::AddressLookup(e) => write!(
927 f,
928 "Error looking up the wallet account for a transparent address: {e}"
929 ),
930 }
931 }
932}
933
934impl<E: std::error::Error + 'static> std::error::Error for ScanBlockError<E> {
935 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
936 match self {
937 ScanBlockError::Scan(e) => Some(e),
938 ScanBlockError::AddressLookup(e) => Some(e),
939 }
940 }
941}
942
943pub fn scan_block<P, AccountId, IvkTag, E>(
951 params: &P,
952 block: CompactBlock,
953 scanning_keys: &ScanningKeys<AccountId, IvkTag>,
954 spend_ids: &SpendIdentifiers<AccountId>,
955 prior_block_metadata: Option<&BlockMetadata>,
956 #[cfg(feature = "transparent-inputs")] find_account_for_address: impl Fn(
957 &TransparentAddress,
958 ) -> Result<
959 Option<(AccountId, Option<TransparentKeyScope>)>,
960 E,
961 >,
962) -> Result<ScannedBlock<AccountId>, ScanBlockError<E>>
963where
964 P: consensus::Parameters + Send + 'static,
965 AccountId: Default + Eq + Hash + ConditionallySelectable + Send + Sync + 'static,
966 IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
967{
968 compact::scan_block_with_runners::<_, _, _, (), (), (), _>(
969 params,
970 block,
971 scanning_keys,
972 spend_ids,
973 prior_block_metadata,
974 None,
975 #[cfg(feature = "transparent-inputs")]
976 find_account_for_address,
977 )
978}
979
980struct PositionTracker {
986 sapling_tree_position: u32,
987 sapling_final_tree_size: u32,
988 #[cfg(feature = "orchard")]
989 orchard_tree_position: u32,
990 #[cfg(feature = "orchard")]
991 orchard_final_tree_size: u32,
992 #[cfg(feature = "orchard")]
993 ironwood_tree_position: u32,
994 #[cfg(feature = "orchard")]
995 ironwood_final_tree_size: u32,
996}
997
998impl PositionTracker {
999 fn sapling_note_position(&self, output_idx: usize) -> Position {
1000 Position::from(u64::from(
1001 self.sapling_tree_position + u32::try_from(output_idx).unwrap(),
1002 ))
1003 }
1004
1005 #[cfg(feature = "orchard")]
1006 fn orchard_note_position(&self, output_idx: usize) -> Position {
1007 Position::from(u64::from(
1008 self.orchard_tree_position + u32::try_from(output_idx).unwrap(),
1009 ))
1010 }
1011
1012 #[cfg(feature = "orchard")]
1013 fn ironwood_note_position(&self, output_idx: usize) -> Position {
1014 Position::from(u64::from(
1015 self.ironwood_tree_position + u32::try_from(output_idx).unwrap(),
1016 ))
1017 }
1018}
1019
1020fn find_spent<'a, I, AccountId, Spend, Nf, WS>(
1023 spends: I,
1024 nullifiers: &[(AccountId, Nf)],
1025 extract_nf: impl Fn(&Spend) -> Nf,
1026 construct_wallet_spend: impl Fn(usize, Nf, AccountId) -> WS,
1027) -> (Vec<WS>, Vec<Nf>)
1028where
1029 I: IntoIterator<Item = &'a Spend>,
1030 I::IntoIter: ExactSizeIterator,
1031 Spend: 'a,
1032 AccountId: ConditionallySelectable + Default,
1033 Nf: ConstantTimeEq + Copy,
1034{
1035 let spends = spends.into_iter();
1036 let mut found_spent = vec![];
1039 let mut unlinked_nullifiers = Vec::with_capacity(spends.len());
1040 for (index, spend) in spends.enumerate() {
1041 let spend_nf = extract_nf(spend);
1042
1043 let ct_spend = nullifiers
1046 .iter()
1047 .map(|&(account, nf)| CtOption::new(account, nf.ct_eq(&spend_nf)))
1048 .fold(
1049 CtOption::new(AccountId::default(), 0.into()),
1050 |first, next| CtOption::conditional_select(&next, &first, first.is_some()),
1051 )
1052 .map(|account| construct_wallet_spend(index, spend_nf, account));
1053
1054 if let Some(spend) = ct_spend.into() {
1055 found_spent.push(spend);
1056 } else {
1057 unlinked_nullifiers.push(spend_nf);
1060 }
1061 }
1062
1063 (found_spent, unlinked_nullifiers)
1064}
1065
1066#[cfg(feature = "transparent-inputs")]
1078fn find_transparent_spends<AccountId: Copy>(
1079 prevouts: impl IntoIterator<Item = OutPoint>,
1080 tracked: &HashMap<OutPoint, AccountId>,
1081) -> (Vec<WalletTransparentSpend<AccountId>>, Vec<OutPoint>) {
1082 let mut found_spent = vec![];
1083 let mut unlinked_prevouts = vec![];
1084
1085 for (index, prevout) in prevouts.into_iter().enumerate() {
1086 match tracked.get(&prevout) {
1087 Some(account_id) => found_spent.push(WalletTransparentSpend::from_parts(
1088 index,
1089 prevout,
1090 *account_id,
1091 )),
1092 None => unlinked_prevouts.push(prevout),
1093 }
1094 }
1095
1096 (found_spent, unlinked_prevouts)
1097}
1098
1099#[allow(clippy::too_many_arguments)]
1100#[allow(clippy::type_complexity)]
1101fn find_received<
1102 AccountId: Copy + Eq + Hash,
1103 D: BatchDomain,
1104 M,
1105 Nf,
1106 IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
1107 SK: ScanningKeyOps<D, AccountId, Nf>,
1108 Output: ShieldedOutput<D>,
1109 NoteCommitment,
1110 Note,
1111>(
1112 block_height: BlockHeight,
1113 last_commitments_in_block: bool,
1114 txid: TxId,
1115 note_position: impl Fn(usize) -> Position,
1116 keys: &HashMap<IvkTag, SK>,
1117 spent_from_accounts: &HashSet<AccountId>,
1118 decoded: &[(D, Output)],
1119 batch_results: Option<impl FnOnce(TxId) -> HashMap<usize, DecryptedOutput<IvkTag, D, M>>>,
1120 decrypt_inline: impl FnOnce(
1121 &[D::IncomingViewingKey],
1122 &[(D, Output)],
1123 ) -> Vec<Option<((D::Note, D::Recipient, M), usize)>>,
1124 extract_note_commitment: impl Fn(&Output) -> NoteCommitment,
1125 enrich_note: impl Fn(D::Note) -> Note,
1126) -> (
1127 Vec<WalletOutput<Note, Nf, AccountId>>,
1128 Vec<(NoteCommitment, Retention<BlockHeight>)>,
1129) {
1130 let (decrypted_opts, decrypted_len) = if let Some(collect_results) = batch_results {
1132 let mut decrypted = collect_results(txid);
1133 let decrypted_len = decrypted.len();
1134 (
1135 (0..decoded.len())
1136 .map(|i| {
1137 decrypted
1138 .remove(&i)
1139 .map(|d_out| (d_out.ivk_tag, d_out.note))
1140 })
1141 .collect::<Vec<_>>(),
1142 decrypted_len,
1143 )
1144 } else {
1145 let mut ivks = Vec::with_capacity(keys.len());
1146 let mut ivk_lookup = Vec::with_capacity(keys.len());
1147 for (key_id, key) in keys.iter() {
1148 ivks.push(key.prepare());
1149 ivk_lookup.push(key_id);
1150 }
1151
1152 let mut decrypted_len = 0;
1153 (
1154 decrypt_inline(&ivks, decoded)
1155 .into_iter()
1156 .map(|v| {
1157 v.map(|((note, _, _), ivk_idx)| {
1158 decrypted_len += 1;
1159 (*ivk_lookup[ivk_idx], note)
1160 })
1161 })
1162 .collect::<Vec<_>>(),
1163 decrypted_len,
1164 )
1165 };
1166
1167 let mut shielded_outputs = Vec::with_capacity(decrypted_len);
1168 let mut note_commitments = Vec::with_capacity(decoded.len());
1169 for (output_idx, ((_, output), decrypted_note)) in
1170 decoded.iter().zip(decrypted_opts).enumerate()
1171 {
1172 let node = extract_note_commitment(output);
1174 let is_checkpoint = output_idx + 1 == decoded.len() && last_commitments_in_block;
1176 let retention = match (decrypted_note.is_some(), is_checkpoint) {
1177 (is_marked, true) => Retention::Checkpoint {
1178 id: block_height,
1179 marking: if is_marked {
1180 Marking::Marked
1181 } else {
1182 Marking::None
1183 },
1184 },
1185 (true, false) => Retention::Marked,
1186 (false, false) => Retention::Ephemeral,
1187 };
1188
1189 if let Some((key_id, note)) = decrypted_note {
1190 let key = keys
1191 .get(&key_id)
1192 .expect("Key is available for decrypted output");
1193
1194 let is_change = spent_from_accounts.contains(key.account_id());
1201 let note_commitment_tree_position = note_position(output_idx);
1202 let nf = key.nf(¬e, note_commitment_tree_position);
1203
1204 shielded_outputs.push(WalletOutput::from_parts(
1205 output_idx,
1206 output.ephemeral_key(),
1207 enrich_note(note),
1208 is_change,
1209 note_commitment_tree_position,
1210 nf,
1211 *key.account_id(),
1212 key.key_scope(),
1213 ));
1214 }
1215
1216 note_commitments.push((node, retention))
1217 }
1218
1219 (shielded_outputs, note_commitments)
1220}
1221
1222#[cfg(any(test, feature = "test-dependencies"))]
1223pub mod testing {
1224 use group::{
1225 GroupEncoding,
1226 ff::{Field, PrimeField},
1227 };
1228 use rand::{Rng, rand_core::UnwrapErr, rngs::SysRng};
1229 use sapling::{
1230 Nullifier,
1231 constants::SPENDING_KEY_GENERATOR,
1232 note_encryption::{COMPACT_NOTE_SIZE, SaplingDomain, sapling_note_encryption},
1233 util::generate_random_rseed,
1234 value::NoteValue,
1235 zip32::DiversifiableFullViewingKey,
1236 };
1237 use zcash_note_encryption::Domain;
1238 use zcash_primitives::{
1239 block::BlockHash, transaction::components::sapling::zip212_enforcement,
1240 };
1241 use zcash_protocol::{
1242 consensus::{BlockHeight, Network},
1243 memo::MemoBytes,
1244 value::Zatoshis,
1245 };
1246
1247 use crate::proto::compact_formats::{
1248 self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
1249 };
1250
1251 fn random_compact_tx(mut rng: impl Rng) -> CompactTx {
1252 let fake_nf = {
1253 let mut nf = vec![0; 32];
1254 rng.fill_bytes(&mut nf);
1255 nf
1256 };
1257 let fake_cmu = {
1258 let fake_cmu = bls12_381::Scalar::random(&mut rng);
1259 fake_cmu.to_repr().to_vec()
1260 };
1261 let fake_epk = {
1262 let mut buffer = [0; 64];
1263 rng.fill_bytes(&mut buffer);
1264 let fake_esk = jubjub::Fr::from_bytes_wide(&buffer);
1265 let fake_epk = SPENDING_KEY_GENERATOR * fake_esk;
1266 fake_epk.to_bytes().to_vec()
1267 };
1268 let cspend = CompactSaplingSpend { nf: fake_nf };
1269 let cout = CompactSaplingOutput {
1270 cmu: fake_cmu,
1271 ephemeral_key: fake_epk,
1272 ciphertext: vec![0; COMPACT_NOTE_SIZE],
1273 };
1274 let mut ctx = CompactTx::default();
1275 let mut txid = vec![0; 32];
1276 rng.fill_bytes(&mut txid);
1277 ctx.txid = txid;
1278 ctx.spends.push(cspend);
1279 ctx.outputs.push(cout);
1280 ctx
1281 }
1282
1283 pub fn fake_compact_block(
1290 height: BlockHeight,
1291 prev_hash: BlockHash,
1292 nf: Nullifier,
1293 dfvk: &DiversifiableFullViewingKey,
1294 value: Zatoshis,
1295 tx_after: bool,
1296 initial_tree_sizes: Option<(u32, u32)>,
1297 ) -> CompactBlock {
1298 let zip212_enforcement = zip212_enforcement(&Network::TestNetwork, height);
1299 let to = dfvk.default_address().1;
1300
1301 let mut rng = UnwrapErr(SysRng);
1303 let rseed = generate_random_rseed(zip212_enforcement, &mut rng);
1304 let note = sapling::Note::from_parts(to, NoteValue::from_raw(value.into()), rseed);
1305 let encryptor = sapling_note_encryption(
1306 Some(dfvk.fvk().ovk),
1307 note.clone(),
1308 MemoBytes::empty().into_bytes(),
1309 &mut rng,
1310 );
1311 let cmu = note.cmu().to_bytes().to_vec();
1312 let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec();
1313 let enc_ciphertext = encryptor.encrypt_note_plaintext();
1314
1315 let mut cb = CompactBlock {
1317 hash: {
1318 let mut hash = vec![0; 32];
1319 rng.fill_bytes(&mut hash);
1320 hash
1321 },
1322 prev_hash: prev_hash.0.to_vec(),
1323 height: height.into(),
1324 ..Default::default()
1325 };
1326
1327 {
1329 let mut tx = random_compact_tx(&mut rng);
1330 tx.index = cb.vtx.len() as u64;
1331 cb.vtx.push(tx);
1332 }
1333
1334 let cspend = CompactSaplingSpend { nf: nf.0.to_vec() };
1335 let cout = CompactSaplingOutput {
1336 cmu,
1337 ephemeral_key,
1338 ciphertext: enc_ciphertext.0[..COMPACT_NOTE_SIZE].to_vec(),
1339 };
1340 let mut ctx = CompactTx::default();
1341 let mut txid = vec![0; 32];
1342 rng.fill_bytes(&mut txid);
1343 ctx.txid = txid;
1344 ctx.spends.push(cspend);
1345 ctx.outputs.push(cout);
1346 ctx.index = cb.vtx.len() as u64;
1347 cb.vtx.push(ctx);
1348
1349 if tx_after {
1351 let mut tx = random_compact_tx(&mut rng);
1352 tx.index = cb.vtx.len() as u64;
1353 cb.vtx.push(tx);
1354 }
1355
1356 cb.chain_metadata =
1357 initial_tree_sizes.map(|(initial_sapling_tree_size, initial_orchard_tree_size)| {
1358 compact::ChainMetadata {
1359 sapling_commitment_tree_size: initial_sapling_tree_size
1360 + cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::<u32>(),
1361 orchard_commitment_tree_size: initial_orchard_tree_size
1362 + cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::<u32>(),
1363 ironwood_commitment_tree_size: 0,
1365 }
1366 });
1367
1368 cb
1369 }
1370}
1371
1372#[cfg(test)]
1373mod tests {
1374 use std::{sync::Arc, thread::spawn};
1375
1376 use super::ScanningKeys;
1377
1378 #[test]
1379 fn arc_scanning_keys() {
1380 let keys = Arc::new(ScanningKeys::<(), ()>::empty());
1381 spawn(move || {
1382 let _ = keys.sapling().get(&());
1383 });
1384 }
1385}