1use std::{collections::BTreeMap, hash::Hash, ops::Range};
2#[cfg(feature = "orchard")]
3use {
4 crate::data_api::ORCHARD_SHARD_HEIGHT, shardtree::store::Checkpoint, std::collections::BTreeSet,
5};
6
7use rayon::{
8 iter::{IndexedParallelIterator as _, ParallelIterator},
9 slice::ParallelSliceMut as _,
10};
11use tracing::{debug, info, trace, warn};
12
13use incrementalmerkletree::{Hashable, Marking, Position, Retention, frontier::Frontier};
14use shardtree::{LocatedPrunableTree, ShardTree, error::ShardTreeError, store::ShardStore};
15use transparent::{address::TransparentAddress, bundle::OutPoint};
16use zcash_keys::{address::Receiver, encoding::AddressCodec as _};
17use zcash_primitives::transaction::Transaction;
18use zcash_protocol::{
19 PoolType, ShieldedPool,
20 consensus::{self, BlockHeight},
21 value::{BalanceError, Zatoshis},
22};
23use zcash_script::solver::ScriptKind;
24
25use crate::{
26 TransferType,
27 data_api::{
28 DecryptedTransaction, SAPLING_SHARD_HEIGHT, ScannedBlock, TransactionStatus,
29 WalletCommitmentTrees, anchor_retention::AnchorRetention, chain::ChainState,
30 ll::ReceivedShieldedOutput,
31 },
32 wallet::{Recipient, WalletTransparentOutput},
33};
34
35use super::{LowLevelWalletRead, LowLevelWalletWrite, TxMeta};
36
37#[cfg(feature = "orchard")]
38use crate::data_api::anchor_retention::{AnchorRetentionInterval, PoolMigrationParams};
39
40#[cfg(feature = "transparent-inputs")]
41use {
42 crate::data_api::Account,
43 std::collections::HashSet,
44 transparent::keys::TransparentKeyScope,
45 zcash_keys::keys::{
46 ReceiverRequirement::*,
47 UnifiedAddressRequest,
48 transparent::gap_limits::{
49 AddressStore, GapAddressesError, GapLimits, generate_gap_addresses,
50 },
51 },
52};
53
54pub(crate) const PRUNING_DEPTH: u32 = 100;
58
59pub(crate) fn determine_fee<DbT, T: TxMeta>(
60 _wallet_db: &DbT,
61 tx: &T,
62) -> Result<Option<Zatoshis>, DbT::Error>
63where
64 DbT: LowLevelWalletRead,
65 DbT::Error: From<BalanceError>,
66{
67 tx.fee_paid(|_outpoint| {
68 #[cfg(not(feature = "transparent-inputs"))]
69 {
70 Ok(None)
74 }
75
76 #[cfg(feature = "transparent-inputs")]
78 if let Some(out) = _wallet_db.get_wallet_transparent_output(_outpoint, None)? {
79 Ok(Some(out.txout().value()))
80 } else {
81 Ok(None)
83 }
84 })
85}
86
87#[cfg(feature = "transparent-inputs")]
92pub fn generate_transparent_gap_addresses<DbT, SE>(
93 wallet_db: &mut DbT,
94 gap_limits: GapLimits,
95 account_id: <DbT as LowLevelWalletRead>::AccountId,
96 key_scope: TransparentKeyScope,
97 request: UnifiedAddressRequest,
98) -> Result<(), GapAddressesError<SE>>
99where
100 DbT: LowLevelWalletWrite<Error = SE>
101 + AddressStore<Error = SE, AccountRef = <DbT as LowLevelWalletRead>::AccountRef>,
102 DbT::TxRef: Eq + Hash,
103{
104 let account_ref = wallet_db
105 .get_account_ref(account_id)
106 .map_err(GapAddressesError::Storage)?;
107
108 let account = wallet_db
109 .get_account_internal(account_ref)
110 .map_err(GapAddressesError::Storage)?
111 .ok_or(GapAddressesError::AccountUnknown)?;
112
113 generate_gap_addresses(
114 wallet_db,
115 &gap_limits,
116 account_ref,
117 &account.uivk(),
118 account.ufvk(),
119 key_scope,
120 request,
121 false,
122 )?;
123
124 Ok(())
125}
126
127#[derive(Debug)]
128#[non_exhaustive]
129pub enum PutBlocksError<SE, TE> {
130 NonSequentialBlocks {
132 prev_height: BlockHeight,
133 block_height: BlockHeight,
134 },
135 Storage(SE),
137 ShardTree(ShardTreeError<TE>),
139 ShardTreeForBlockRange {
145 pool: ShieldedPool,
147 block_range: Range<BlockHeight>,
150 error: ShardTreeError<TE>,
152 },
153 #[cfg(feature = "transparent-inputs")]
154 GapAddresses(GapAddressesError<SE>),
155}
156
157impl<SE, TE> From<ShardTreeError<TE>> for PutBlocksError<SE, TE> {
158 fn from(value: ShardTreeError<TE>) -> Self {
159 PutBlocksError::ShardTree(value)
160 }
161}
162
163#[cfg(feature = "transparent-inputs")]
164impl<SE, TE> From<GapAddressesError<SE>> for PutBlocksError<SE, TE> {
165 fn from(value: GapAddressesError<SE>) -> Self {
166 PutBlocksError::GapAddresses(value)
167 }
168}
169
170#[cfg(not(feature = "transparent-inputs"))]
172pub trait PutBlocksDbT<SE, TE, AR>:
173 LowLevelWalletWrite<Error = SE> + WalletCommitmentTrees<Error = TE>
174{
175}
176
177#[cfg(not(feature = "transparent-inputs"))]
178impl<T: LowLevelWalletWrite<Error = SE> + WalletCommitmentTrees<Error = TE>, SE, TE, AR>
179 PutBlocksDbT<SE, TE, AR> for T
180{
181}
182
183#[cfg(feature = "transparent-inputs")]
191pub trait PutBlocksDbT<SE, TE, AR>:
192 LowLevelWalletWrite<Error = SE>
193 + WalletCommitmentTrees<Error = TE>
194 + AddressStore<Error = SE, AccountRef = AR>
195{
196}
197
198#[cfg(feature = "transparent-inputs")]
199impl<
200 T: LowLevelWalletWrite<Error = SE>
201 + WalletCommitmentTrees<Error = TE>
202 + AddressStore<Error = SE, AccountRef = AR>,
203 SE,
204 TE,
205 AR,
206> PutBlocksDbT<SE, TE, AR> for T
207{
208}
209
210#[cfg(not(feature = "transparent-inputs"))]
215pub trait PutBlocksRowsDbT<SE, AR>: LowLevelWalletWrite<Error = SE> {}
216
217#[cfg(not(feature = "transparent-inputs"))]
218impl<T: LowLevelWalletWrite<Error = SE>, SE, AR> PutBlocksRowsDbT<SE, AR> for T {}
219
220#[cfg(feature = "transparent-inputs")]
231pub trait PutBlocksRowsDbT<SE, AR>:
232 LowLevelWalletWrite<Error = SE> + AddressStore<Error = SE, AccountRef = AR>
233{
234}
235
236#[cfg(feature = "transparent-inputs")]
237impl<T: LowLevelWalletWrite<Error = SE> + AddressStore<Error = SE, AccountRef = AR>, SE, AR>
238 PutBlocksRowsDbT<SE, AR> for T
239{
240}
241
242#[derive(Default)]
249pub struct PutBlocksRows {
250 pub sapling_commitments: Vec<Option<(sapling::Node, Retention<BlockHeight>)>>,
253 #[cfg(feature = "orchard")]
256 pub orchard_commitments:
257 Vec<Option<(orchard::tree::MerkleHashOrchard, Retention<BlockHeight>)>>,
258 #[cfg(feature = "orchard")]
261 pub ironwood_commitments:
262 Vec<Option<(orchard::tree::MerkleHashOrchard, Retention<BlockHeight>)>>,
263 pub note_positions: Vec<(ShieldedPool, Position)>,
266 pub last_scanned_height: Option<BlockHeight>,
269}
270
271pub fn put_blocks_rows<DbT, SE, TE>(
315 wallet_db: &mut DbT,
316 #[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
317 from_state: &ChainState,
318 blocks: Vec<ScannedBlock<<DbT as LowLevelWalletRead>::AccountId>>,
319) -> Result<PutBlocksRows, PutBlocksError<SE, TE>>
320where
321 DbT: PutBlocksRowsDbT<SE, <DbT as LowLevelWalletRead>::AccountRef>,
322 DbT::TxRef: Eq + Hash,
323{
324 if blocks.is_empty() {
325 return Ok(PutBlocksRows::default());
326 }
327
328 let initial_block = blocks.first().expect("blocks is known to be nonempty");
329 let mut initial_block_sequential = from_state.block_height() + 1 == initial_block.height();
330 {
331 initial_block_sequential &= from_state.final_sapling_tree().tree_size()
332 + u64::try_from(initial_block.sapling().commitments().len()).unwrap()
333 == u64::from(initial_block.sapling().final_tree_size());
334 }
335 #[cfg(feature = "orchard")]
336 {
337 initial_block_sequential &= from_state.final_orchard_tree().tree_size()
338 + u64::try_from(initial_block.orchard().commitments().len()).unwrap()
339 == u64::from(initial_block.orchard().final_tree_size());
340 initial_block_sequential &= from_state.final_ironwood_tree().tree_size()
341 + u64::try_from(initial_block.ironwood().commitments().len()).unwrap()
342 == u64::from(initial_block.ironwood().final_tree_size());
343 }
344 if !initial_block_sequential {
345 return Err(PutBlocksError::NonSequentialBlocks {
346 prev_height: from_state.block_height(),
347 block_height: initial_block.height(),
348 });
349 }
350
351 let spend_tracking_floor = spend_tracking_floor(
352 wallet_db
353 .block_fully_scanned_height()
354 .map_err(PutBlocksError::Storage)?,
355 from_state.block_height(),
356 blocks.last().map(|block| block.height()),
357 );
358
359 let mut sapling_commitments = vec![];
360 #[cfg(feature = "orchard")]
361 let mut orchard_commitments = vec![];
362 #[cfg(feature = "orchard")]
363 let mut ironwood_commitments = vec![];
364 let mut last_scanned_height = None;
365 let mut note_positions = vec![];
366
367 #[cfg(feature = "transparent-inputs")]
368 let mut tx_refs = HashSet::new();
369
370 for block in blocks.into_iter() {
371 if last_scanned_height
372 .iter()
373 .any(|prev| block.height() != *prev + 1)
374 {
375 return Err(PutBlocksError::NonSequentialBlocks {
376 prev_height: last_scanned_height.expect("last scanned height is known"),
377 block_height: block.height(),
378 });
379 }
380
381 wallet_db
383 .put_block_meta(
384 block.height(),
385 block.block_hash(),
386 block.block_time(),
387 block.sapling().final_tree_size(),
388 block.sapling().commitments().len().try_into().unwrap(),
389 #[cfg(feature = "orchard")]
390 block.orchard().final_tree_size(),
391 #[cfg(feature = "orchard")]
392 block.orchard().commitments().len().try_into().unwrap(),
393 #[cfg(feature = "orchard")]
394 block.ironwood().final_tree_size(),
395 #[cfg(feature = "orchard")]
396 block.ironwood().commitments().len().try_into().unwrap(),
397 )
398 .map_err(PutBlocksError::Storage)?;
399
400 for tx in block.transactions() {
401 let tx_ref = wallet_db
402 .put_tx_meta(tx, block.height())
403 .map_err(PutBlocksError::Storage)?;
404
405 #[cfg(feature = "transparent-inputs")]
406 tx_refs.insert(tx_ref);
407
408 wallet_db
409 .queue_tx_retrieval(std::iter::once(tx.txid()), None)
410 .map_err(PutBlocksError::Storage)?;
411
412 let _ = mark_notes_spent(
416 wallet_db,
417 tx_ref,
418 #[cfg(feature = "transparent-inputs")]
419 tx.transparent_spends().iter().map(|spend| spend.outpoint()),
420 tx.sapling_spends().iter().map(|spend| spend.nf()),
421 #[cfg(feature = "orchard")]
422 tx.orchard_spends().iter().map(|spend| spend.nf()),
423 #[cfg(feature = "orchard")]
424 tx.ironwood_spends().iter().map(|spend| spend.nf()),
425 )
426 .map_err(PutBlocksError::Storage)?;
427
428 let params: Option<&consensus::Network> = None;
430
431 put_shielded_outputs(
432 wallet_db,
433 params,
434 tx_ref,
435 None,
436 tx.sapling_outputs(),
437 |wallet_db, output| {
440 Ok(output
441 .nf()
442 .map(|nf| wallet_db.detect_sapling_spend(nf))
443 .transpose()?
444 .flatten())
445 },
446 |wallet_db, output, tx_ref, spent_in| {
447 wallet_db.put_received_sapling_note(
448 output,
449 tx_ref,
450 Some(block.height()),
451 spent_in,
452 )
453 },
454 |_account_id| (),
455 )
456 .map_err(PutBlocksError::Storage)?;
457
458 #[cfg(feature = "orchard")]
459 put_shielded_outputs(
460 wallet_db,
461 params,
462 tx_ref,
463 None,
464 tx.orchard_outputs(),
465 |wallet_db, output| {
468 Ok(output
469 .nf()
470 .map(|nf| wallet_db.detect_orchard_spend(nf))
471 .transpose()?
472 .flatten())
473 },
474 |wallet_db, output, tx_ref, spent_in| {
475 wallet_db.put_received_orchard_note(
476 output,
477 tx_ref,
478 Some(block.height()),
479 spent_in,
480 )
481 },
482 |_account_id| (),
483 )
484 .map_err(PutBlocksError::Storage)?;
485
486 #[cfg(feature = "orchard")]
487 put_shielded_outputs(
488 wallet_db,
489 params,
490 tx_ref,
491 None,
492 tx.ironwood_outputs(),
493 |wallet_db, output| {
496 Ok(output
497 .nf()
498 .map(|nf| wallet_db.detect_ironwood_spend(nf))
499 .transpose()?
500 .flatten())
501 },
502 |wallet_db, output, tx_ref, spent_in| {
503 wallet_db.put_received_ironwood_note(
504 output,
505 tx_ref,
506 Some(block.height()),
507 spent_in,
508 )
509 },
510 |_account_id| (),
511 )
512 .map_err(PutBlocksError::Storage)?;
513
514 #[cfg(feature = "transparent-inputs")]
521 for output in tx.transparent_outputs() {
522 put_received_transparent_output(
523 wallet_db,
524 tx_ref,
525 output,
526 block.height(),
527 |_, _| (),
528 )
529 .map_err(PutBlocksError::Storage)?;
530 }
531 }
532
533 if should_track_spends(spend_tracking_floor, block.height()) {
536 wallet_db
537 .track_block_sapling_nullifiers(block.height(), block.sapling().nullifier_map())
538 .map_err(PutBlocksError::Storage)?;
539
540 #[cfg(feature = "orchard")]
541 wallet_db
542 .track_block_orchard_nullifiers(block.height(), block.orchard().nullifier_map())
543 .map_err(PutBlocksError::Storage)?;
544
545 #[cfg(feature = "orchard")]
546 wallet_db
547 .track_block_ironwood_nullifiers(block.height(), block.ironwood().nullifier_map())
548 .map_err(PutBlocksError::Storage)?;
549
550 #[cfg(feature = "transparent-inputs")]
551 wallet_db
552 .track_block_transparent_spends(block.height(), block.transparent_spend_map())
553 .map_err(PutBlocksError::Storage)?;
554 }
555
556 note_positions.extend(block.transactions().iter().flat_map(|wtx| {
557 let iter = wtx
558 .sapling_outputs()
559 .iter()
560 .map(|out| (ShieldedPool::Sapling, out.note_commitment_tree_position()));
561 #[cfg(feature = "orchard")]
562 let iter = iter.chain(
563 wtx.orchard_outputs()
564 .iter()
565 .map(|out| (ShieldedPool::Orchard, out.note_commitment_tree_position())),
566 );
567 #[cfg(feature = "orchard")]
568 let iter = iter.chain(
569 wtx.ironwood_outputs()
570 .iter()
571 .map(|out| (ShieldedPool::Ironwood, out.note_commitment_tree_position())),
572 );
573
574 iter
575 }));
576
577 last_scanned_height = Some(block.height());
578 let block_commitments = block.into_commitments();
579 trace!(
580 "Sapling commitments for {:?}: {:?}",
581 last_scanned_height,
582 block_commitments
583 .sapling
584 .iter()
585 .map(|(_, r)| *r)
586 .collect::<Vec<_>>()
587 );
588 #[cfg(feature = "orchard")]
589 trace!(
590 "Orchard commitments for {:?}: {:?}",
591 last_scanned_height,
592 block_commitments
593 .orchard
594 .iter()
595 .map(|(_, r)| *r)
596 .collect::<Vec<_>>()
597 );
598
599 sapling_commitments.extend(block_commitments.sapling.into_iter().map(Some));
600 #[cfg(feature = "orchard")]
601 orchard_commitments.extend(block_commitments.orchard.into_iter().map(Some));
602 #[cfg(feature = "orchard")]
603 ironwood_commitments.extend(block_commitments.ironwood.into_iter().map(Some));
604 }
605
606 #[cfg(feature = "transparent-inputs")]
607 for (account_id, key_scope) in wallet_db
608 .find_involved_accounts(tx_refs)
609 .map_err(PutBlocksError::Storage)?
610 {
611 if let Some(t_key_scope) = key_scope {
612 generate_transparent_gap_addresses(
613 wallet_db,
614 gap_limits,
615 account_id,
616 t_key_scope,
617 UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
618 )
619 .map_err(PutBlocksError::GapAddresses)?;
620 }
621 }
622
623 wallet_db
625 .prune_tracked_spends(PRUNING_DEPTH)
626 .map_err(PutBlocksError::Storage)?;
627
628 Ok(PutBlocksRows {
629 sapling_commitments,
630 #[cfg(feature = "orchard")]
631 orchard_commitments,
632 #[cfg(feature = "orchard")]
633 ironwood_commitments,
634 note_positions,
635 last_scanned_height,
636 })
637}
638
639pub fn put_blocks<DbT, SE, TE>(
661 wallet_db: &mut DbT,
662 #[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
663 from_state: &ChainState,
664 blocks: Vec<ScannedBlock<<DbT as LowLevelWalletRead>::AccountId>>,
665 anchor_retention: Option<&AnchorRetention>,
666) -> Result<(), PutBlocksError<SE, TE>>
667where
668 DbT: PutBlocksDbT<SE, TE, <DbT as LowLevelWalletRead>::AccountRef>,
669 DbT::TxRef: Eq + Hash,
670{
671 let rows = put_blocks_rows(
672 wallet_db,
673 #[cfg(feature = "transparent-inputs")]
674 gap_limits,
675 from_state,
676 blocks,
677 )?;
678
679 let mut sapling_commitments = rows.sapling_commitments;
680 #[cfg(feature = "orchard")]
681 let mut orchard_commitments = rows.orchard_commitments;
682 #[cfg(feature = "orchard")]
683 let mut ironwood_commitments = rows.ironwood_commitments;
684 let note_positions = rows.note_positions;
685 let last_scanned_height = rows.last_scanned_height;
686
687 if let Some(last_scanned_height) = last_scanned_height {
690 const CHUNK_SIZE: usize = 1024;
692 let sapling_subtrees = build_subtrees::<_, SAPLING_SHARD_HEIGHT>(
693 Position::from(from_state.final_sapling_tree().tree_size()),
694 &mut sapling_commitments,
695 CHUNK_SIZE,
696 );
697
698 #[cfg(feature = "orchard")]
699 let orchard_subtrees = build_subtrees::<_, ORCHARD_SHARD_HEIGHT>(
700 Position::from(from_state.final_orchard_tree().tree_size()),
701 &mut orchard_commitments,
702 CHUNK_SIZE,
703 );
704
705 #[cfg(feature = "orchard")]
708 let ironwood_subtrees = build_subtrees::<_, ORCHARD_SHARD_HEIGHT>(
709 Position::from(from_state.final_ironwood_tree().tree_size()),
710 &mut ironwood_commitments,
711 CHUNK_SIZE,
712 );
713
714 #[cfg(feature = "orchard")]
726 let (
727 missing_sapling_checkpoints,
728 missing_orchard_checkpoints,
729 missing_ironwood_checkpoints,
730 ) = {
731 let sapling_checkpoint_positions = checkpoint_positions(&sapling_subtrees);
732 let orchard_checkpoint_positions = checkpoint_positions(&orchard_subtrees);
733 let ironwood_checkpoint_positions = checkpoint_positions(&ironwood_subtrees);
734
735 let [ensure_sapling, ensure_orchard, ensure_ironwood] = batch_ensure_heights(
736 &sapling_checkpoint_positions.keys().copied().collect(),
737 &orchard_checkpoint_positions.keys().copied().collect(),
738 &ironwood_checkpoint_positions.keys().copied().collect(),
739 anchor_retention,
740 from_state.block_height() + 1..=last_scanned_height,
741 );
742
743 (
744 ensure_checkpoints(
745 ensure_sapling.iter(),
746 &sapling_checkpoint_positions,
747 from_state.final_sapling_tree(),
748 ),
749 ensure_checkpoints(
750 ensure_orchard.iter(),
751 &orchard_checkpoint_positions,
752 from_state.final_orchard_tree(),
753 ),
754 ensure_checkpoints(
755 ensure_ironwood.iter(),
756 &ironwood_checkpoint_positions,
757 from_state.final_ironwood_tree(),
758 ),
759 )
760 };
761
762 {
764 let mut sapling_subtrees = sapling_subtrees.into_iter();
765 #[cfg(feature = "orchard")]
766 let mut missing_checkpoints = missing_sapling_checkpoints.into_iter();
767 wallet_db.with_sapling_tree_mut(|sapling_tree| {
768 update_tree(
769 "Sapling",
770 from_state.final_sapling_tree(),
771 from_state.block_height(),
772 sapling_tree,
773 anchor_retention,
774 &mut sapling_subtrees,
775 #[cfg(feature = "orchard")]
776 &mut missing_checkpoints,
777 )
778 .map_err(|error| PutBlocksError::ShardTreeForBlockRange {
779 pool: ShieldedPool::Sapling,
780 block_range: from_state.block_height()..(last_scanned_height + 1),
781 error,
782 })
783 })?;
784 }
785
786 #[cfg(feature = "orchard")]
788 {
789 let mut orchard_subtrees = orchard_subtrees.into_iter();
790 let mut missing_checkpoints = missing_orchard_checkpoints.into_iter();
791 wallet_db.with_orchard_tree_mut(|orchard_tree| {
792 update_tree(
793 "Orchard",
794 from_state.final_orchard_tree(),
795 from_state.block_height(),
796 orchard_tree,
797 anchor_retention,
798 &mut orchard_subtrees,
799 &mut missing_checkpoints,
800 )
801 .map_err(|error| PutBlocksError::ShardTreeForBlockRange {
802 pool: ShieldedPool::Orchard,
803 block_range: from_state.block_height()..(last_scanned_height + 1),
804 error,
805 })
806 })?;
807 }
808
809 #[cfg(feature = "orchard")]
811 {
812 let mut ironwood_subtrees = ironwood_subtrees.into_iter();
813 let mut missing_checkpoints = missing_ironwood_checkpoints.into_iter();
814 wallet_db.with_ironwood_tree_mut(|ironwood_tree| {
815 update_tree(
816 "Ironwood",
817 from_state.final_ironwood_tree(),
818 from_state.block_height(),
819 ironwood_tree,
820 anchor_retention,
821 &mut ironwood_subtrees,
822 &mut missing_checkpoints,
823 )
824 .map_err(|error| PutBlocksError::ShardTreeForBlockRange {
825 pool: ShieldedPool::Ironwood,
826 block_range: from_state.block_height()..(last_scanned_height + 1),
827 error,
828 })
829 })?;
830 }
831
832 wallet_db
833 .notify_scan_complete(
834 Range {
835 start: from_state.block_height() + 1,
836 end: last_scanned_height + 1,
837 },
838 ¬e_positions,
839 )
840 .map_err(PutBlocksError::Storage)?;
841 }
842
843 Ok(())
844}
845
846#[cfg(not(feature = "transparent-inputs"))]
847type GapError<DbT> = <DbT as LowLevelWalletRead>::Error;
848
849#[cfg(not(feature = "transparent-inputs"))]
851pub trait StoreDecryptedTxDbT: LowLevelWalletWrite {}
852
853#[cfg(not(feature = "transparent-inputs"))]
854impl<T: LowLevelWalletWrite> StoreDecryptedTxDbT for T {}
855
856#[cfg(feature = "transparent-inputs")]
857type GapError<DbT> = GapAddressesError<<DbT as LowLevelWalletRead>::Error>;
858
859#[cfg(feature = "transparent-inputs")]
867pub trait StoreDecryptedTxDbT:
868 LowLevelWalletWrite
869 + AddressStore<
870 Error = <Self as LowLevelWalletRead>::Error,
871 AccountRef = <Self as LowLevelWalletRead>::AccountRef,
872 >
873where
874 <Self as LowLevelWalletRead>::Error: From<GapError<Self>>,
875{
876}
877
878#[cfg(feature = "transparent-inputs")]
879impl<
880 T: LowLevelWalletWrite
881 + AddressStore<
882 Error = <T as LowLevelWalletRead>::Error,
883 AccountRef = <T as LowLevelWalletRead>::AccountRef,
884 >,
885> StoreDecryptedTxDbT for T
886where
887 <T as LowLevelWalletRead>::Error: From<GapError<T>>,
888{
889}
890
891pub fn store_decrypted_tx<DbT, P>(
914 wallet_db: &mut DbT,
915 params: &P,
916 #[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
917 chain_tip_height: BlockHeight,
918 d_tx: DecryptedTransaction<Transaction, <DbT as LowLevelWalletRead>::AccountId>,
919) -> Result<(), <DbT as LowLevelWalletRead>::Error>
920where
921 DbT: StoreDecryptedTxDbT,
922 <DbT as LowLevelWalletRead>::AccountId: core::fmt::Debug,
923 <DbT as LowLevelWalletRead>::Error: From<BalanceError> + From<GapError<DbT>>,
924 P: consensus::Parameters,
925{
926 let funding_accounts = wallet_db.get_funding_accounts(d_tx.tx())?;
927
928 let funding_account = funding_accounts.iter().next().copied();
930 if funding_accounts.len() > 1 {
931 warn!(
932 "More than one wallet account detected as funding transaction {:?}, selecting {:?}",
933 d_tx.tx().txid(),
934 funding_account.unwrap()
935 )
936 }
937
938 let wallet_transparent_outputs =
939 detect_wallet_transparent_outputs::<_, _, <DbT as LowLevelWalletRead>::Error>(
940 params,
941 d_tx.tx(),
942 d_tx.mined_height(),
943 funding_account,
944 #[cfg(feature = "transparent-inputs")]
945 |address| wallet_db.find_account_for_transparent_address(address),
946 )?;
947
948 if funding_account.is_none()
951 && wallet_transparent_outputs.is_empty()
952 && !d_tx.has_decrypted_outputs()
953 {
954 wallet_db.delete_retrieval_queue_entries(d_tx.tx().txid())?;
955 return Ok(());
956 }
957
958 info!("Storing decrypted transaction with id {}", d_tx.tx().txid());
959 let observed_height = d_tx.mined_height().unwrap_or(chain_tip_height + 1);
960
961 let fee = determine_fee(wallet_db, d_tx.tx())?;
964
965 let tx_ref = wallet_db.put_tx_data(d_tx.tx(), fee, None, None, observed_height)?;
966 if let Some(height) = d_tx.mined_height() {
967 wallet_db.set_transaction_status(d_tx.tx().txid(), TransactionStatus::Mined(height))?;
968 }
969
970 #[cfg(feature = "orchard")]
983 {
984 let migration_params = PoolMigrationParams::new(
985 zcash_protocol::zip318::BlockSpacing::for_network(params),
986 AnchorRetentionInterval::default(),
987 );
988 let classification = crate::data_api::zip318::classify_decrypted_tx(
989 d_tx.tx(),
990 d_tx.orchard_outputs(),
991 d_tx.ironwood_outputs(),
992 &migration_params,
993 );
994 wallet_db.put_zip318_classification(tx_ref, classification)?;
995 }
996
997 let has_wallet_shielded_spend = mark_notes_spent(
998 wallet_db,
999 tx_ref,
1000 #[cfg(feature = "transparent-inputs")]
1001 d_tx.tx()
1002 .transparent_bundle()
1003 .iter()
1004 .flat_map(|b| b.vin.iter())
1005 .map(|txin| txin.prevout()),
1006 d_tx.tx()
1007 .sapling_bundle()
1008 .iter()
1009 .flat_map(|b| b.shielded_spends().iter())
1010 .map(|spend| spend.nullifier()),
1011 #[cfg(feature = "orchard")]
1012 d_tx.tx()
1013 .orchard_bundle()
1014 .iter()
1015 .flat_map(|b| b.actions().iter())
1016 .map(|action| action.nullifier()),
1017 #[cfg(feature = "orchard")]
1018 d_tx.tx()
1019 .ironwood_bundle()
1020 .iter()
1021 .flat_map(|b| b.actions().iter())
1022 .map(|action| action.nullifier()),
1023 )?;
1024
1025 #[cfg(feature = "transparent-inputs")]
1029 let mut tx_has_wallet_outputs = false;
1030 #[cfg(feature = "transparent-inputs")]
1031 {
1032 tx_has_wallet_outputs |= !d_tx.sapling_outputs().is_empty();
1033
1034 #[cfg(feature = "orchard")]
1035 {
1036 tx_has_wallet_outputs |= !d_tx.orchard_outputs().is_empty();
1037 tx_has_wallet_outputs |= !d_tx.ironwood_outputs().is_empty();
1038 }
1039
1040 tx_has_wallet_outputs |= !wallet_transparent_outputs.is_empty();
1047 }
1048
1049 #[cfg(feature = "transparent-inputs")]
1051 let mut gap_update_set = HashSet::new();
1052
1053 put_shielded_outputs(
1054 wallet_db,
1055 Some(params),
1056 tx_ref,
1057 funding_account,
1058 d_tx.sapling_outputs(),
1059 |_, _| Ok(None),
1060 |wallet_db, output, tx_ref, spent_in| {
1061 wallet_db.put_received_sapling_note(output, tx_ref, d_tx.mined_height(), spent_in)
1062 },
1063 |_account_id| {
1064 #[cfg(feature = "transparent-inputs")]
1065 gap_update_set.insert((_account_id, TransparentKeyScope::EXTERNAL));
1066 },
1067 )?;
1068
1069 #[cfg(feature = "orchard")]
1070 put_shielded_outputs(
1071 wallet_db,
1072 Some(params),
1073 tx_ref,
1074 funding_account,
1075 d_tx.orchard_outputs(),
1076 |_, _| Ok(None),
1077 |wallet_db, output, tx_ref, spent_in| {
1078 wallet_db.put_received_orchard_note(output, tx_ref, d_tx.mined_height(), spent_in)
1079 },
1080 |_account_id| {
1081 #[cfg(feature = "transparent-inputs")]
1082 gap_update_set.insert((_account_id, TransparentKeyScope::EXTERNAL));
1083 },
1084 )?;
1085
1086 #[cfg(feature = "orchard")]
1089 put_shielded_outputs(
1090 wallet_db,
1091 Some(params),
1092 tx_ref,
1093 funding_account,
1094 d_tx.ironwood_outputs(),
1095 |_, _| Ok(None),
1096 |wallet_db, output, tx_ref, spent_in| {
1097 wallet_db.put_received_ironwood_note(output, tx_ref, d_tx.mined_height(), spent_in)
1098 },
1099 |_account_id| {
1100 #[cfg(feature = "transparent-inputs")]
1101 gap_update_set.insert((_account_id, TransparentKeyScope::EXTERNAL));
1102 },
1103 )?;
1104
1105 put_transparent_outputs(
1106 wallet_db,
1107 params,
1108 tx_ref,
1109 &wallet_transparent_outputs,
1110 #[cfg(feature = "transparent-inputs")]
1111 observed_height,
1112 #[cfg(feature = "transparent-inputs")]
1113 |account_id, t_key_scope| {
1114 gap_update_set.insert((account_id, t_key_scope));
1115 },
1116 )?;
1117
1118 #[cfg(feature = "transparent-inputs")]
1120 for (account_id, key_scope) in gap_update_set {
1121 generate_transparent_gap_addresses(
1122 wallet_db,
1123 gap_limits,
1124 account_id,
1125 key_scope,
1126 UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
1127 )?;
1128 }
1129
1130 for (spending_tx_ref, spending_tx) in wallet_db.get_unknown_fee_spenders_of(tx_ref)? {
1133 if let Some(fee) = determine_fee(wallet_db, &spending_tx)? {
1134 wallet_db.update_tx_fee(spending_tx_ref, fee)?;
1135 }
1136 }
1137
1138 #[cfg(feature = "transparent-inputs")]
1143 if tx_has_wallet_outputs {
1144 wallet_db.queue_transparent_input_retrieval(tx_ref, &d_tx)?
1145 }
1146
1147 wallet_db.delete_retrieval_queue_entries(d_tx.tx().txid())?;
1150
1151 if d_tx.mined_height().is_none() && !(has_wallet_shielded_spend || d_tx.has_decrypted_outputs())
1155 {
1156 wallet_db.queue_tx_status(d_tx.tx().txid())?;
1157 }
1158
1159 Ok(())
1160}
1161
1162pub(crate) fn detect_wallet_transparent_outputs<P, AccountId, E>(
1163 params: &P,
1164 tx: &Transaction,
1165 mined_height: Option<BlockHeight>,
1166 funding_account: Option<AccountId>,
1167 #[cfg(feature = "transparent-inputs")] find_account_for_address: impl Fn(
1168 &TransparentAddress,
1169 ) -> Result<
1170 Option<(AccountId, Option<TransparentKeyScope>)>,
1171 E,
1172 >,
1173) -> Result<Vec<WalletTransparentOutput<AccountId>>, E>
1174where
1175 P: consensus::Parameters,
1176 AccountId: Copy + core::fmt::Debug + std::hash::Hash + std::cmp::Eq,
1177{
1178 let mut result = vec![];
1179 for (output_index, txout) in tx
1180 .transparent_bundle()
1181 .iter()
1182 .flat_map(|b| b.vout.iter())
1183 .enumerate()
1184 {
1185 let script_kind = txout.script_kind();
1186 if let Some(address) = script_kind
1187 .as_ref()
1188 .and_then(TransparentAddress::from_script_kind)
1189 {
1190 debug!(
1191 "{:?} output {} has recipient {}",
1192 tx.txid(),
1193 output_index,
1194 address.encode(params)
1195 );
1196
1197 #[allow(unused_mut)]
1198 let mut detected = false;
1199
1200 #[cfg(feature = "transparent-inputs")]
1202 if let Some((account_uuid, key_scope)) = find_account_for_address(&address)? {
1203 debug!(
1204 "{:?} output {} belongs to account {:?}",
1205 tx.txid(),
1206 output_index,
1207 account_uuid
1208 );
1209 result.push(
1210 WalletTransparentOutput::from_parts(
1211 OutPoint::new(tx.txid().into(), u32::try_from(output_index).unwrap()),
1212 txout.clone(),
1213 mined_height,
1214 Some(account_uuid),
1215 key_scope,
1216 funding_account,
1217 )
1218 .expect("txout.recipient_address extraction previously checked"),
1219 );
1220 detected = true;
1221 } else {
1222 debug!(
1223 "Address {} is not recognized as belonging to any of our accounts.",
1224 address.encode(params)
1225 );
1226 }
1227
1228 if !detected {
1229 if let Some(account_id) = funding_account {
1233 result.push(
1234 WalletTransparentOutput::from_parts(
1235 OutPoint::new(tx.txid().into(), u32::try_from(output_index).unwrap()),
1236 txout.clone(),
1237 mined_height,
1238 None,
1239 None,
1240 Some(account_id),
1241 )
1242 .expect("txout.recipient_address extraction previously checked"),
1243 );
1244 }
1245 }
1246 } else if let Some(script_kind) = script_kind {
1247 if !matches!(script_kind, ScriptKind::NullData { .. }) {
1251 warn!(
1252 "Ignoring unsupported script kind '{}' for tx {} output {}",
1253 script_kind.as_str(),
1254 tx.txid(),
1255 output_index
1256 );
1257 }
1258 } else {
1259 warn!(
1260 "Unable to determine recipient address for tx {} output {}",
1261 tx.txid(),
1262 output_index
1263 );
1264 }
1265 }
1266
1267 Ok(result)
1268}
1269
1270fn mark_notes_spent<'a, DbT>(
1271 wallet_db: &mut DbT,
1272 tx_ref: <DbT as LowLevelWalletRead>::TxRef,
1273 #[cfg(feature = "transparent-inputs")] transparent_prevouts: impl Iterator<
1274 Item = &'a transparent::bundle::OutPoint,
1275 >,
1276 sapling_nfs: impl Iterator<Item = &'a sapling::Nullifier>,
1277 #[cfg(feature = "orchard")] orchard_nfs: impl Iterator<Item = &'a orchard::note::Nullifier>,
1278 #[cfg(feature = "orchard")] ironwood_nfs: impl Iterator<Item = &'a orchard::note::Nullifier>,
1279) -> Result<bool, <DbT as LowLevelWalletRead>::Error>
1280where
1281 DbT: LowLevelWalletWrite,
1282{
1283 let mut has_wallet_shielded_spend = false;
1284
1285 #[cfg(feature = "transparent-inputs")]
1287 for outpoint in transparent_prevouts {
1288 wallet_db.mark_transparent_utxo_spent(outpoint, tx_ref)?;
1289 }
1290
1291 for nf in sapling_nfs {
1293 has_wallet_shielded_spend |= wallet_db.mark_sapling_note_spent(nf, tx_ref)?;
1294 }
1295
1296 #[cfg(feature = "orchard")]
1298 for nf in orchard_nfs {
1299 has_wallet_shielded_spend |= wallet_db.mark_orchard_note_spent(nf, tx_ref)?;
1300 }
1301
1302 #[cfg(feature = "orchard")]
1304 for nf in ironwood_nfs {
1305 has_wallet_shielded_spend |= wallet_db.mark_ironwood_note_spent(nf, tx_ref)?;
1306 }
1307
1308 Ok(has_wallet_shielded_spend)
1309}
1310
1311#[allow(clippy::too_many_arguments)]
1312fn put_shielded_outputs<DbT, P, Output>(
1313 wallet_db: &mut DbT,
1314 params: Option<&P>,
1315 tx_ref: <DbT as LowLevelWalletRead>::TxRef,
1316 funding_account: Option<DbT::AccountId>,
1317 outputs: &[Output],
1318 detect_note_spent_in: impl Fn(
1319 &mut DbT,
1320 &Output,
1321 ) -> Result<
1322 Option<<DbT as LowLevelWalletRead>::TxRef>,
1323 <DbT as LowLevelWalletRead>::Error,
1324 >,
1325 put_received_note: impl Fn(
1326 &mut DbT,
1327 &Output,
1328 <DbT as LowLevelWalletRead>::TxRef,
1329 Option<<DbT as LowLevelWalletRead>::TxRef>,
1330 ) -> Result<(), <DbT as LowLevelWalletRead>::Error>,
1331 mut on_external_account: impl FnMut(<DbT as LowLevelWalletRead>::AccountId),
1332) -> Result<(), <DbT as LowLevelWalletRead>::Error>
1333where
1334 DbT: LowLevelWalletWrite,
1335 P: consensus::Parameters,
1336 Output: ReceivedShieldedOutput<AccountId = <DbT as LowLevelWalletRead>::AccountId>,
1337{
1338 for output in outputs {
1339 let sent_output = match output.transfer_type() {
1340 TransferType::Outgoing => {
1341 let note = output.to_wallet_note();
1342
1343 let recipient = Recipient::External {
1344 recipient_address: external_address(
1345 wallet_db,
1346 params.expect("present when outgoing is possible (store_decrypted_tx)"),
1347 output.account_id(),
1348 note.receiver(),
1349 )?,
1350 output_pool: PoolType::Shielded(note.pool()),
1351 };
1352
1353 Some((output.account_id(), recipient, note.value()))
1354 }
1355 TransferType::AccountInternal => {
1356 let spent_in = detect_note_spent_in(wallet_db, output)?;
1357 put_received_note(wallet_db, output, tx_ref, spent_in)?;
1358
1359 let note = output.to_wallet_note();
1360 let value = note.value();
1361
1362 let recipient = Recipient::InternalShielded {
1363 receiving_account: output.account_id(),
1364 external_address: None,
1365 note: Box::new(note),
1366 };
1367
1368 Some((output.account_id(), recipient, value))
1369 }
1370 TransferType::Incoming => {
1371 let spent_in = detect_note_spent_in(wallet_db, output)?;
1372 put_received_note(wallet_db, output, tx_ref, spent_in)?;
1373 on_external_account(output.account_id());
1374
1375 if let Some(account_id) = funding_account {
1376 let note = output.to_wallet_note();
1377 let value = note.value();
1378
1379 let recipient = Recipient::InternalShielded {
1381 receiving_account: output.account_id(),
1382 external_address: Some(external_address(
1383 wallet_db,
1384 params.expect(
1385 "present when funding_account is known (store_decrypted_tx)",
1386 ),
1387 output.account_id(),
1388 note.receiver(),
1389 )?),
1390 note: Box::new(note),
1391 };
1392
1393 Some((account_id, recipient, value))
1394 } else {
1395 None
1396 }
1397 }
1398 TransferType::WalletInternal => unreachable!(
1399 "TransferType::WalletInternal is only produced for transparent outputs"
1400 ),
1401 };
1402
1403 if let Some((from_account_uuid, recipient, value)) = sent_output {
1404 wallet_db.put_sent_output(
1405 from_account_uuid,
1406 tx_ref,
1407 output.index(),
1408 &recipient,
1409 value,
1410 output.memo(),
1411 )?;
1412 }
1413 }
1414
1415 Ok(())
1416}
1417
1418#[cfg(feature = "transparent-inputs")]
1435fn put_received_transparent_output<DbT>(
1436 wallet_db: &mut DbT,
1437 tx_ref: <DbT as LowLevelWalletRead>::TxRef,
1438 output: &WalletTransparentOutput<<DbT as LowLevelWalletRead>::AccountId>,
1439 observation_height: BlockHeight,
1440 mut on_received: impl FnMut(<DbT as LowLevelWalletRead>::AccountId, TransparentKeyScope),
1441) -> Result<(), <DbT as LowLevelWalletRead>::Error>
1442where
1443 DbT: LowLevelWalletWrite,
1444{
1445 if output.recipient_account().is_none() {
1446 return Ok(());
1447 }
1448
1449 let (account_id, _) = wallet_db.put_transparent_output(output, observation_height, false)?;
1450
1451 if let Some(t_key_scope) = output.recipient_key_scope() {
1452 on_received(account_id, t_key_scope);
1453 }
1454
1455 wallet_db.queue_transparent_spend_detection(
1468 *output.recipient_address(),
1469 tx_ref,
1470 output.outpoint().n(),
1471 )?;
1472
1473 Ok(())
1474}
1475
1476fn put_transparent_outputs<DbT, P>(
1483 wallet_db: &mut DbT,
1484 params: &P,
1485 tx_ref: <DbT as LowLevelWalletRead>::TxRef,
1486 outputs: &[WalletTransparentOutput<<DbT as LowLevelWalletRead>::AccountId>],
1487 #[cfg(feature = "transparent-inputs")] observation_height: BlockHeight,
1488 #[cfg(feature = "transparent-inputs")] mut on_received: impl FnMut(
1489 <DbT as LowLevelWalletRead>::AccountId,
1490 TransparentKeyScope,
1491 ),
1492) -> Result<(), <DbT as LowLevelWalletRead>::Error>
1493where
1494 DbT: LowLevelWalletWrite,
1495 P: consensus::Parameters,
1496{
1497 for output in outputs {
1498 #[cfg(feature = "transparent-inputs")]
1501 put_received_transparent_output(
1502 wallet_db,
1503 tx_ref,
1504 output,
1505 observation_height,
1506 &mut on_received,
1507 )?;
1508
1509 if let Some(&from_account) = output.funding_account() {
1513 let recipient = match output.recipient_account() {
1514 #[cfg(feature = "transparent-inputs")]
1515 Some(&receiving_account) => Recipient::InternalTransparent {
1516 receiving_account,
1517 recipient_address: *output.recipient_address(),
1518 },
1519 #[cfg(not(feature = "transparent-inputs"))]
1520 Some(_) => Recipient::External {
1521 recipient_address: Receiver::Transparent(*output.recipient_address())
1522 .to_zcash_address(params.network_type()),
1523 output_pool: PoolType::TRANSPARENT,
1524 },
1525 None => {
1526 let receiver = Receiver::Transparent(*output.recipient_address());
1527
1528 #[cfg(feature = "transparent-inputs")]
1529 let recipient_address =
1530 external_address(wallet_db, params, from_account, receiver)?;
1531
1532 #[cfg(not(feature = "transparent-inputs"))]
1533 let recipient_address = receiver.to_zcash_address(params.network_type());
1534
1535 Recipient::External {
1536 recipient_address,
1537 output_pool: PoolType::TRANSPARENT,
1538 }
1539 }
1540 };
1541
1542 wallet_db.put_sent_output(
1543 from_account,
1544 tx_ref,
1545 output.index(),
1546 &recipient,
1547 output.value(),
1548 None,
1549 )?;
1550 }
1551 }
1552
1553 Ok(())
1554}
1555
1556fn external_address<DbT, P>(
1558 wallet_db: &DbT,
1559 params: &P,
1560 account_id: DbT::AccountId,
1561 receiver: Receiver,
1562) -> Result<zcash_address::ZcashAddress, <DbT as LowLevelWalletRead>::Error>
1563where
1564 DbT: LowLevelWalletRead,
1565 P: consensus::Parameters,
1566{
1567 let recipient_address = wallet_db
1568 .select_receiving_address(account_id, &receiver)?
1569 .unwrap_or_else(|| receiver.to_zcash_address(params.network_type()));
1570
1571 Ok(recipient_address)
1572}
1573
1574pub fn build_subtrees<H, const SHARD_HEIGHT: u8>(
1583 start_position: Position,
1584 commitments: &mut [Option<(H, Retention<BlockHeight>)>],
1585 chunk_size: usize,
1586) -> Vec<(LocatedPrunableTree<H>, BTreeMap<BlockHeight, Position>)>
1587where
1588 H: Clone + PartialEq + Hashable + Send + Sync,
1589{
1590 commitments
1591 .par_chunks_mut(chunk_size)
1592 .enumerate()
1593 .filter_map(|(i, chunk)| {
1594 let start = start_position + (i * chunk_size) as u64;
1595 let end = start + chunk.len() as u64;
1596
1597 shardtree::LocatedTree::from_iter(
1598 start..end,
1599 SHARD_HEIGHT.into(),
1600 chunk.iter_mut().map(|n| n.take().expect("always Some")),
1601 )
1602 })
1603 .map(|res| (res.subtree, res.checkpoints))
1604 .collect()
1605}
1606
1607#[cfg(feature = "orchard")]
1609pub fn checkpoint_positions<H>(
1610 subtrees: &[(LocatedPrunableTree<H>, BTreeMap<BlockHeight, Position>)],
1611) -> BTreeMap<BlockHeight, Position> {
1612 subtrees
1613 .iter()
1614 .flat_map(|(_, checkpoints)| checkpoints.iter())
1615 .map(|(k, v)| (*k, *v))
1616 .collect()
1617}
1618
1619#[cfg(feature = "orchard")]
1624pub fn ensure_checkpoints<'a, H, I: Iterator<Item = &'a BlockHeight>, const DEPTH: u8>(
1625 ensure_heights: I,
1628 existing_checkpoint_positions: &BTreeMap<BlockHeight, Position>,
1631 state_final_tree: &Frontier<H, DEPTH>,
1634) -> Vec<(BlockHeight, Checkpoint)> {
1635 ensure_heights
1636 .flat_map(|ensure_height| {
1637 existing_checkpoint_positions
1638 .range::<BlockHeight, _>(..=*ensure_height)
1639 .last()
1640 .map_or_else(
1641 || {
1642 Some((
1643 *ensure_height,
1644 state_final_tree
1645 .value()
1646 .map_or_else(Checkpoint::tree_empty, |t| {
1647 Checkpoint::at_position(t.position())
1648 }),
1649 ))
1650 },
1651 |(existing_checkpoint_height, position)| {
1652 if *existing_checkpoint_height < *ensure_height {
1653 Some((*ensure_height, Checkpoint::at_position(*position)))
1654 } else {
1655 None
1658 }
1659 },
1660 )
1661 .into_iter()
1662 })
1663 .collect::<Vec<_>>()
1664}
1665
1666pub const SPEND_MAP_RETENTION_BLOCKS: u32 = 100;
1674
1675fn spend_tracking_floor(
1683 fully_scanned: Option<BlockHeight>,
1684 from_state_height: BlockHeight,
1685 batch_end: Option<BlockHeight>,
1686) -> Option<BlockHeight> {
1687 if fully_scanned == Some(from_state_height) {
1688 batch_end.and_then(|last| {
1689 let floor =
1690 BlockHeight::from(u32::from(last).saturating_sub(SPEND_MAP_RETENTION_BLOCKS));
1691 (floor > from_state_height + 1).then_some(floor)
1692 })
1693 } else {
1694 None
1695 }
1696}
1697
1698fn should_track_spends(
1705 spend_tracking_floor: Option<BlockHeight>,
1706 block_height: BlockHeight,
1707) -> bool {
1708 spend_tracking_floor.is_none_or(|floor| block_height >= floor)
1709}
1710
1711fn should_retain_anchor(anchor_retention: Option<&AnchorRetention>, height: BlockHeight) -> bool {
1715 anchor_retention.is_some_and(|retention| retention.retains(height))
1716}
1717
1718fn retain_anchor_checkpoint<S, const DEPTH: u8, const SHARD_HEIGHT: u8>(
1720 tree: &mut ShardTree<S, DEPTH, SHARD_HEIGHT>,
1721 anchor_retention: Option<&AnchorRetention>,
1722 height: BlockHeight,
1723) -> Result<(), ShardTreeError<S::Error>>
1724where
1725 S: ShardStore<CheckpointId = BlockHeight>,
1726 S::H: Clone + PartialEq + Hashable,
1727{
1728 if should_retain_anchor(anchor_retention, height) {
1729 tree.ensure_retained(height)?;
1730 }
1731 Ok(())
1732}
1733
1734#[cfg(feature = "orchard")]
1745pub fn cross_pool_ensure_heights(
1746 sapling: &BTreeSet<BlockHeight>,
1747 orchard: &BTreeSet<BlockHeight>,
1748 ironwood: &BTreeSet<BlockHeight>,
1749) -> [BTreeSet<BlockHeight>; 3] {
1750 let union = |a: &BTreeSet<BlockHeight>, b: &BTreeSet<BlockHeight>| {
1751 a.union(b).copied().collect::<BTreeSet<BlockHeight>>()
1752 };
1753 [
1754 union(orchard, ironwood),
1755 union(sapling, ironwood),
1756 union(sapling, orchard),
1757 ]
1758}
1759
1760#[cfg(feature = "orchard")]
1784pub fn batch_ensure_heights(
1785 sapling: &BTreeSet<BlockHeight>,
1786 orchard: &BTreeSet<BlockHeight>,
1787 ironwood: &BTreeSet<BlockHeight>,
1788 anchor_retention: Option<&AnchorRetention>,
1789 range: std::ops::RangeInclusive<BlockHeight>,
1790) -> [BTreeSet<BlockHeight>; 3] {
1791 let mut ensure = cross_pool_ensure_heights(sapling, orchard, ironwood);
1792
1793 if let Some(retention) = anchor_retention {
1794 let retained = retention.retained_in_range(range);
1795 for pool in ensure.iter_mut() {
1796 pool.extend(retained.iter().copied());
1797 }
1798 }
1799
1800 ensure
1801}
1802
1803pub fn update_tree<S, const DEPTH: u8, const SHARD_HEIGHT: u8>(
1813 protocol: &'static str,
1814 frontier: &Frontier<S::H, DEPTH>,
1815 frontier_height: BlockHeight,
1816 tree: &mut ShardTree<S, DEPTH, SHARD_HEIGHT>,
1817 anchor_retention: Option<&AnchorRetention>,
1818 subtrees: impl Iterator<Item = (LocatedPrunableTree<S::H>, BTreeMap<BlockHeight, Position>)>,
1819 #[cfg(feature = "orchard")] missing_checkpoints: impl Iterator<Item = (BlockHeight, Checkpoint)>,
1820) -> Result<(), ShardTreeError<S::Error>>
1821where
1822 S: ShardStore<CheckpointId = BlockHeight>,
1823 S::H: Clone + PartialEq + Hashable,
1824{
1825 debug!(
1826 "{protocol} initial tree size at {frontier_height:?}: {:?}",
1827 frontier.tree_size()
1828 );
1829 tree.insert_frontier(
1832 frontier.clone(),
1833 Retention::Checkpoint {
1834 id: frontier_height,
1835 marking: Marking::Reference,
1836 },
1837 )?;
1838 retain_anchor_checkpoint(tree, anchor_retention, frontier_height)?;
1839
1840 for (subtree, checkpoints) in subtrees {
1841 for height in checkpoints.keys() {
1847 retain_anchor_checkpoint(tree, anchor_retention, *height)?;
1848 }
1849 tree.insert_tree(subtree, checkpoints)?;
1850 }
1851
1852 #[cfg(feature = "orchard")]
1856 {
1857 let min_checkpoint_height = tree
1858 .store()
1859 .min_checkpoint_id()
1860 .map_err(ShardTreeError::Storage)?
1861 .expect("At least one checkpoint was inserted (by insert_frontier)");
1862
1863 for (height, checkpoint) in missing_checkpoints {
1864 if height > min_checkpoint_height {
1865 debug!(
1866 "Adding missing {protocol} checkpoint for height: {:?}: {:?}",
1867 height,
1868 checkpoint.position()
1869 );
1870 tree.store_mut()
1871 .add_checkpoint(height, checkpoint.clone())
1872 .map_err(ShardTreeError::Storage)?;
1873 retain_anchor_checkpoint(tree, anchor_retention, height)?;
1874 }
1875 }
1876 }
1877
1878 Ok(())
1879}
1880
1881#[cfg(test)]
1882mod tests {
1883 #[cfg(feature = "orchard")]
1884 use {super::cross_pool_ensure_heights, std::collections::BTreeSet};
1885
1886 use core::num::NonZeroU32;
1887
1888 use proptest::prelude::*;
1889 use zcash_protocol::consensus::BlockHeight;
1890
1891 #[cfg(feature = "orchard")]
1892 use super::batch_ensure_heights;
1893 use super::{
1894 SPEND_MAP_RETENTION_BLOCKS, should_retain_anchor, should_track_spends, spend_tracking_floor,
1895 };
1896 use crate::data_api::anchor_retention::{AnchorRetention, AnchorRetentionInterval};
1897
1898 #[test]
1902 fn out_of_order_ranges_track_fully() {
1903 let h = BlockHeight::from;
1904 assert_eq!(
1906 spend_tracking_floor(Some(h(1_000)), h(500_000), Some(h(510_000))),
1907 None
1908 );
1909 assert_eq!(
1911 spend_tracking_floor(None, h(500_000), Some(h(510_000))),
1912 None
1913 );
1914 assert_eq!(
1916 spend_tracking_floor(Some(h(600_000)), h(500_000), Some(h(510_000))),
1917 None
1918 );
1919 }
1920
1921 #[test]
1925 fn frontier_batches_retain_the_trailing_window() {
1926 let from = BlockHeight::from(500_000);
1927 let last = BlockHeight::from(510_000);
1928 let floor = spend_tracking_floor(Some(from), from, Some(last)).expect("frontier ⇒ floor");
1929 assert_eq!(
1930 u32::from(last) - u32::from(floor),
1931 SPEND_MAP_RETENTION_BLOCKS
1932 );
1933
1934 let short = BlockHeight::from(500_000 + SPEND_MAP_RETENTION_BLOCKS / 2);
1935 assert_eq!(spend_tracking_floor(Some(from), from, Some(short)), None);
1936 assert_eq!(spend_tracking_floor(Some(from), from, None), None);
1937 }
1938
1939 #[test]
1940 fn spend_tracking_floor_gating() {
1941 let floor = BlockHeight::from(1000);
1942
1943 assert!(should_track_spends(None, BlockHeight::from(0)));
1945 assert!(should_track_spends(None, BlockHeight::from(999)));
1946
1947 assert!(should_track_spends(Some(floor), BlockHeight::from(1000)));
1949 assert!(should_track_spends(Some(floor), BlockHeight::from(1001)));
1950
1951 assert!(!should_track_spends(Some(floor), BlockHeight::from(999)));
1953 assert!(!should_track_spends(Some(floor), BlockHeight::from(0)));
1954 }
1955
1956 #[test]
1959 fn anchor_retention_gating() {
1960 for interval in [
1961 AnchorRetentionInterval::ZIP_318,
1962 AnchorRetentionInterval::custom(NonZeroU32::new(12).expect("nonzero")),
1963 ] {
1964 let blocks = interval.block_count().get();
1965 let floor = BlockHeight::from(4 * blocks);
1966 let policy = AnchorRetention::new(
1967 floor,
1968 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1969 interval,
1970 );
1971 let retention = Some(&policy);
1972
1973 assert!(!should_retain_anchor(None, BlockHeight::from(8 * blocks)));
1975
1976 assert!(should_retain_anchor(
1978 retention,
1979 BlockHeight::from(4 * blocks)
1980 ));
1981 assert!(should_retain_anchor(
1982 retention,
1983 BlockHeight::from(8 * blocks)
1984 ));
1985
1986 assert!(!should_retain_anchor(
1988 retention,
1989 BlockHeight::from(3 * blocks)
1990 ));
1991
1992 assert!(!should_retain_anchor(
1994 retention,
1995 BlockHeight::from(4 * blocks + 1)
1996 ));
1997 assert!(!should_retain_anchor(
1998 retention,
1999 BlockHeight::from(5 * blocks - 1)
2000 ));
2001 }
2002 }
2003
2004 #[cfg(feature = "orchard")]
2005 prop_compose! {
2006 fn arb_heights()(
2008 heights in proptest::collection::vec(0u32..100, 0..20),
2009 ) -> BTreeSet<BlockHeight> {
2010 heights.into_iter().map(BlockHeight::from).collect()
2011 }
2012 }
2013
2014 #[cfg(feature = "orchard")]
2015 fn union(a: &BTreeSet<BlockHeight>, b: &BTreeSet<BlockHeight>) -> BTreeSet<BlockHeight> {
2016 a.union(b).copied().collect()
2017 }
2018
2019 proptest! {
2020 #[test]
2025 #[cfg(feature = "orchard")]
2026 fn ensure_heights_align_all_pools(
2027 sapling in arb_heights(),
2028 orchard in arb_heights(),
2029 ironwood in arb_heights(),
2030 ) {
2031 let [ensure_sapling, ensure_orchard, ensure_ironwood] =
2032 cross_pool_ensure_heights(&sapling, &orchard, &ironwood);
2033
2034 let total = union(&union(&sapling, &orchard), &ironwood);
2035
2036 prop_assert_eq!(union(&sapling, &ensure_sapling), total.clone());
2037 prop_assert_eq!(union(&orchard, &ensure_orchard), total.clone());
2038 prop_assert_eq!(union(&ironwood, &ensure_ironwood), total);
2039
2040 prop_assert_eq!(ensure_sapling, union(&orchard, &ironwood));
2043 prop_assert_eq!(ensure_orchard, union(&sapling, &ironwood));
2044 prop_assert_eq!(ensure_ironwood, union(&sapling, &orchard));
2045 }
2046
2047 #[test]
2051 #[cfg(feature = "orchard")]
2052 fn ensure_heights_degrade_to_two_pools(
2053 sapling in arb_heights(),
2054 orchard in arb_heights(),
2055 ) {
2056 let [ensure_sapling, ensure_orchard, ensure_ironwood] =
2057 cross_pool_ensure_heights(&sapling, &orchard, &BTreeSet::new());
2058
2059 prop_assert_eq!(ensure_sapling, orchard.clone());
2060 prop_assert_eq!(ensure_orchard, sapling.clone());
2061 prop_assert_eq!(ensure_ironwood, union(&sapling, &orchard));
2062 }
2063 }
2064
2065 #[cfg(feature = "orchard")]
2074 #[test]
2075 fn retained_boundary_on_a_commitment_free_block_is_ensured() {
2076 let h = BlockHeight::from;
2077 let retention = AnchorRetention::new(
2080 h(1_000),
2081 zcash_protocol::zip318::BlockSpacing::UNIFORM,
2082 AnchorRetentionInterval::custom(NonZeroU32::new(12).unwrap()),
2083 );
2084
2085 let (sap_cp, orch_cp, iw_cp) = (
2086 BTreeSet::from([h(1_198)]),
2087 BTreeSet::from([h(1_205)]),
2088 BTreeSet::new(),
2089 );
2090
2091 for heights in cross_pool_ensure_heights(&sap_cp, &orch_cp, &iw_cp) {
2095 assert!(
2096 !heights.contains(&h(1_200)),
2097 "cross-pool alignment must not supply the boundary; the union is what does"
2098 );
2099 }
2100
2101 let [sapling, orchard, ironwood] = batch_ensure_heights(
2102 &sap_cp,
2103 &orch_cp,
2104 &iw_cp,
2105 Some(&retention),
2106 h(1_150)..=h(1_250),
2107 );
2108
2109 for (pool, heights) in [
2110 ("sapling", &sapling),
2111 ("orchard", &orchard),
2112 ("ironwood", &ironwood),
2113 ] {
2114 assert!(
2115 heights.contains(&h(1_200)),
2116 "{pool} must ensure the retained boundary 1200, got {heights:?}"
2117 );
2118 }
2119 }
2120
2121 #[cfg(feature = "orchard")]
2125 #[test]
2126 fn no_retention_policy_is_exactly_cross_pool() {
2127 let h = BlockHeight::from;
2128 let sapling = BTreeSet::from([h(100), h(140)]);
2129 let orchard = BTreeSet::from([h(120)]);
2130 let ironwood = BTreeSet::from([h(160)]);
2131
2132 assert_eq!(
2133 batch_ensure_heights(&sapling, &orchard, &ironwood, None, h(1)..=h(1_000)),
2134 cross_pool_ensure_heights(&sapling, &orchard, &ironwood)
2135 );
2136 }
2137}