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>(
312 wallet_db: &mut DbT,
313 #[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
314 from_state: &ChainState,
315 blocks: Vec<ScannedBlock<<DbT as LowLevelWalletRead>::AccountId>>,
316) -> Result<PutBlocksRows, PutBlocksError<SE, TE>>
317where
318 DbT: PutBlocksRowsDbT<SE, <DbT as LowLevelWalletRead>::AccountRef>,
319 DbT::TxRef: Eq + Hash,
320{
321 if blocks.is_empty() {
322 return Ok(PutBlocksRows::default());
323 }
324
325 let initial_block = blocks.first().expect("blocks is known to be nonempty");
326 let mut initial_block_sequential = from_state.block_height() + 1 == initial_block.height();
327 {
328 initial_block_sequential &= from_state.final_sapling_tree().tree_size()
329 + u64::try_from(initial_block.sapling().commitments().len()).unwrap()
330 == u64::from(initial_block.sapling().final_tree_size());
331 }
332 #[cfg(feature = "orchard")]
333 {
334 initial_block_sequential &= from_state.final_orchard_tree().tree_size()
335 + u64::try_from(initial_block.orchard().commitments().len()).unwrap()
336 == u64::from(initial_block.orchard().final_tree_size());
337 initial_block_sequential &= from_state.final_ironwood_tree().tree_size()
338 + u64::try_from(initial_block.ironwood().commitments().len()).unwrap()
339 == u64::from(initial_block.ironwood().final_tree_size());
340 }
341 if !initial_block_sequential {
342 return Err(PutBlocksError::NonSequentialBlocks {
343 prev_height: from_state.block_height(),
344 block_height: initial_block.height(),
345 });
346 }
347
348 let nullifier_tracking_floor = nullifier_tracking_floor(
349 wallet_db
350 .block_fully_scanned_height()
351 .map_err(PutBlocksError::Storage)?,
352 from_state.block_height(),
353 blocks.last().map(|block| block.height()),
354 );
355
356 let mut sapling_commitments = vec![];
357 #[cfg(feature = "orchard")]
358 let mut orchard_commitments = vec![];
359 #[cfg(feature = "orchard")]
360 let mut ironwood_commitments = vec![];
361 let mut last_scanned_height = None;
362 let mut note_positions = vec![];
363
364 #[cfg(feature = "transparent-inputs")]
365 let mut tx_refs = HashSet::new();
366
367 for block in blocks.into_iter() {
368 if last_scanned_height
369 .iter()
370 .any(|prev| block.height() != *prev + 1)
371 {
372 return Err(PutBlocksError::NonSequentialBlocks {
373 prev_height: last_scanned_height.expect("last scanned height is known"),
374 block_height: block.height(),
375 });
376 }
377
378 wallet_db
380 .put_block_meta(
381 block.height(),
382 block.block_hash(),
383 block.block_time(),
384 block.sapling().final_tree_size(),
385 block.sapling().commitments().len().try_into().unwrap(),
386 #[cfg(feature = "orchard")]
387 block.orchard().final_tree_size(),
388 #[cfg(feature = "orchard")]
389 block.orchard().commitments().len().try_into().unwrap(),
390 #[cfg(feature = "orchard")]
391 block.ironwood().final_tree_size(),
392 #[cfg(feature = "orchard")]
393 block.ironwood().commitments().len().try_into().unwrap(),
394 )
395 .map_err(PutBlocksError::Storage)?;
396
397 for tx in block.transactions() {
398 let tx_ref = wallet_db
399 .put_tx_meta(tx, block.height())
400 .map_err(PutBlocksError::Storage)?;
401
402 #[cfg(feature = "transparent-inputs")]
403 tx_refs.insert(tx_ref);
404
405 wallet_db
406 .queue_tx_retrieval(std::iter::once(tx.txid()), None)
407 .map_err(PutBlocksError::Storage)?;
408
409 let _ = mark_notes_spent(
411 wallet_db,
412 tx_ref,
413 #[cfg(feature = "transparent-inputs")]
414 None.iter(),
415 tx.sapling_spends().iter().map(|spend| spend.nf()),
416 #[cfg(feature = "orchard")]
417 tx.orchard_spends().iter().map(|spend| spend.nf()),
418 #[cfg(feature = "orchard")]
419 tx.ironwood_spends().iter().map(|spend| spend.nf()),
420 )
421 .map_err(PutBlocksError::Storage)?;
422
423 let params: Option<&consensus::Network> = None;
425
426 put_shielded_outputs(
427 wallet_db,
428 params,
429 tx_ref,
430 None,
431 tx.sapling_outputs(),
432 |wallet_db, output| {
435 Ok(output
436 .nf()
437 .map(|nf| wallet_db.detect_sapling_spend(nf))
438 .transpose()?
439 .flatten())
440 },
441 |wallet_db, output, tx_ref, spent_in| {
442 wallet_db.put_received_sapling_note(
443 output,
444 tx_ref,
445 Some(block.height()),
446 spent_in,
447 )
448 },
449 |_account_id| (),
450 )
451 .map_err(PutBlocksError::Storage)?;
452
453 #[cfg(feature = "orchard")]
454 put_shielded_outputs(
455 wallet_db,
456 params,
457 tx_ref,
458 None,
459 tx.orchard_outputs(),
460 |wallet_db, output| {
463 Ok(output
464 .nf()
465 .map(|nf| wallet_db.detect_orchard_spend(nf))
466 .transpose()?
467 .flatten())
468 },
469 |wallet_db, output, tx_ref, spent_in| {
470 wallet_db.put_received_orchard_note(
471 output,
472 tx_ref,
473 Some(block.height()),
474 spent_in,
475 )
476 },
477 |_account_id| (),
478 )
479 .map_err(PutBlocksError::Storage)?;
480
481 #[cfg(feature = "orchard")]
482 put_shielded_outputs(
483 wallet_db,
484 params,
485 tx_ref,
486 None,
487 tx.ironwood_outputs(),
488 |wallet_db, output| {
491 Ok(output
492 .nf()
493 .map(|nf| wallet_db.detect_ironwood_spend(nf))
494 .transpose()?
495 .flatten())
496 },
497 |wallet_db, output, tx_ref, spent_in| {
498 wallet_db.put_received_ironwood_note(
499 output,
500 tx_ref,
501 Some(block.height()),
502 spent_in,
503 )
504 },
505 |_account_id| (),
506 )
507 .map_err(PutBlocksError::Storage)?;
508 }
509
510 if should_track_nullifiers(nullifier_tracking_floor, block.height()) {
513 wallet_db
514 .track_block_sapling_nullifiers(block.height(), block.sapling().nullifier_map())
515 .map_err(PutBlocksError::Storage)?;
516
517 #[cfg(feature = "orchard")]
518 wallet_db
519 .track_block_orchard_nullifiers(block.height(), block.orchard().nullifier_map())
520 .map_err(PutBlocksError::Storage)?;
521
522 #[cfg(feature = "orchard")]
523 wallet_db
524 .track_block_ironwood_nullifiers(block.height(), block.ironwood().nullifier_map())
525 .map_err(PutBlocksError::Storage)?;
526 }
527
528 note_positions.extend(block.transactions().iter().flat_map(|wtx| {
529 let iter = wtx
530 .sapling_outputs()
531 .iter()
532 .map(|out| (ShieldedPool::Sapling, out.note_commitment_tree_position()));
533 #[cfg(feature = "orchard")]
534 let iter = iter.chain(
535 wtx.orchard_outputs()
536 .iter()
537 .map(|out| (ShieldedPool::Orchard, out.note_commitment_tree_position())),
538 );
539 #[cfg(feature = "orchard")]
540 let iter = iter.chain(
541 wtx.ironwood_outputs()
542 .iter()
543 .map(|out| (ShieldedPool::Ironwood, out.note_commitment_tree_position())),
544 );
545
546 iter
547 }));
548
549 last_scanned_height = Some(block.height());
550 let block_commitments = block.into_commitments();
551 trace!(
552 "Sapling commitments for {:?}: {:?}",
553 last_scanned_height,
554 block_commitments
555 .sapling
556 .iter()
557 .map(|(_, r)| *r)
558 .collect::<Vec<_>>()
559 );
560 #[cfg(feature = "orchard")]
561 trace!(
562 "Orchard commitments for {:?}: {:?}",
563 last_scanned_height,
564 block_commitments
565 .orchard
566 .iter()
567 .map(|(_, r)| *r)
568 .collect::<Vec<_>>()
569 );
570
571 sapling_commitments.extend(block_commitments.sapling.into_iter().map(Some));
572 #[cfg(feature = "orchard")]
573 orchard_commitments.extend(block_commitments.orchard.into_iter().map(Some));
574 #[cfg(feature = "orchard")]
575 ironwood_commitments.extend(block_commitments.ironwood.into_iter().map(Some));
576 }
577
578 #[cfg(feature = "transparent-inputs")]
579 for (account_id, key_scope) in wallet_db
580 .find_involved_accounts(tx_refs)
581 .map_err(PutBlocksError::Storage)?
582 {
583 if let Some(t_key_scope) = key_scope {
584 generate_transparent_gap_addresses(
585 wallet_db,
586 gap_limits,
587 account_id,
588 t_key_scope,
589 UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
590 )
591 .map_err(PutBlocksError::GapAddresses)?;
592 }
593 }
594
595 wallet_db
597 .prune_tracked_nullifiers(PRUNING_DEPTH)
598 .map_err(PutBlocksError::Storage)?;
599
600 Ok(PutBlocksRows {
601 sapling_commitments,
602 #[cfg(feature = "orchard")]
603 orchard_commitments,
604 #[cfg(feature = "orchard")]
605 ironwood_commitments,
606 note_positions,
607 last_scanned_height,
608 })
609}
610
611pub fn put_blocks<DbT, SE, TE>(
633 wallet_db: &mut DbT,
634 #[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
635 from_state: &ChainState,
636 blocks: Vec<ScannedBlock<<DbT as LowLevelWalletRead>::AccountId>>,
637 anchor_retention: Option<&AnchorRetention>,
638) -> Result<(), PutBlocksError<SE, TE>>
639where
640 DbT: PutBlocksDbT<SE, TE, <DbT as LowLevelWalletRead>::AccountRef>,
641 DbT::TxRef: Eq + Hash,
642{
643 let rows = put_blocks_rows(
644 wallet_db,
645 #[cfg(feature = "transparent-inputs")]
646 gap_limits,
647 from_state,
648 blocks,
649 )?;
650
651 let mut sapling_commitments = rows.sapling_commitments;
652 #[cfg(feature = "orchard")]
653 let mut orchard_commitments = rows.orchard_commitments;
654 #[cfg(feature = "orchard")]
655 let mut ironwood_commitments = rows.ironwood_commitments;
656 let note_positions = rows.note_positions;
657 let last_scanned_height = rows.last_scanned_height;
658
659 if let Some(last_scanned_height) = last_scanned_height {
662 const CHUNK_SIZE: usize = 1024;
664 let sapling_subtrees = build_subtrees::<_, SAPLING_SHARD_HEIGHT>(
665 Position::from(from_state.final_sapling_tree().tree_size()),
666 &mut sapling_commitments,
667 CHUNK_SIZE,
668 );
669
670 #[cfg(feature = "orchard")]
671 let orchard_subtrees = build_subtrees::<_, ORCHARD_SHARD_HEIGHT>(
672 Position::from(from_state.final_orchard_tree().tree_size()),
673 &mut orchard_commitments,
674 CHUNK_SIZE,
675 );
676
677 #[cfg(feature = "orchard")]
680 let ironwood_subtrees = build_subtrees::<_, ORCHARD_SHARD_HEIGHT>(
681 Position::from(from_state.final_ironwood_tree().tree_size()),
682 &mut ironwood_commitments,
683 CHUNK_SIZE,
684 );
685
686 #[cfg(feature = "orchard")]
698 let (
699 missing_sapling_checkpoints,
700 missing_orchard_checkpoints,
701 missing_ironwood_checkpoints,
702 ) = {
703 let sapling_checkpoint_positions = checkpoint_positions(&sapling_subtrees);
704 let orchard_checkpoint_positions = checkpoint_positions(&orchard_subtrees);
705 let ironwood_checkpoint_positions = checkpoint_positions(&ironwood_subtrees);
706
707 let [ensure_sapling, ensure_orchard, ensure_ironwood] = batch_ensure_heights(
708 &sapling_checkpoint_positions.keys().copied().collect(),
709 &orchard_checkpoint_positions.keys().copied().collect(),
710 &ironwood_checkpoint_positions.keys().copied().collect(),
711 anchor_retention,
712 from_state.block_height() + 1..=last_scanned_height,
713 );
714
715 (
716 ensure_checkpoints(
717 ensure_sapling.iter(),
718 &sapling_checkpoint_positions,
719 from_state.final_sapling_tree(),
720 ),
721 ensure_checkpoints(
722 ensure_orchard.iter(),
723 &orchard_checkpoint_positions,
724 from_state.final_orchard_tree(),
725 ),
726 ensure_checkpoints(
727 ensure_ironwood.iter(),
728 &ironwood_checkpoint_positions,
729 from_state.final_ironwood_tree(),
730 ),
731 )
732 };
733
734 {
736 let mut sapling_subtrees = sapling_subtrees.into_iter();
737 #[cfg(feature = "orchard")]
738 let mut missing_checkpoints = missing_sapling_checkpoints.into_iter();
739 wallet_db.with_sapling_tree_mut(|sapling_tree| {
740 update_tree(
741 "Sapling",
742 from_state.final_sapling_tree(),
743 from_state.block_height(),
744 sapling_tree,
745 anchor_retention,
746 &mut sapling_subtrees,
747 #[cfg(feature = "orchard")]
748 &mut missing_checkpoints,
749 )
750 .map_err(|error| PutBlocksError::ShardTreeForBlockRange {
751 pool: ShieldedPool::Sapling,
752 block_range: from_state.block_height()..(last_scanned_height + 1),
753 error,
754 })
755 })?;
756 }
757
758 #[cfg(feature = "orchard")]
760 {
761 let mut orchard_subtrees = orchard_subtrees.into_iter();
762 let mut missing_checkpoints = missing_orchard_checkpoints.into_iter();
763 wallet_db.with_orchard_tree_mut(|orchard_tree| {
764 update_tree(
765 "Orchard",
766 from_state.final_orchard_tree(),
767 from_state.block_height(),
768 orchard_tree,
769 anchor_retention,
770 &mut orchard_subtrees,
771 &mut missing_checkpoints,
772 )
773 .map_err(|error| PutBlocksError::ShardTreeForBlockRange {
774 pool: ShieldedPool::Orchard,
775 block_range: from_state.block_height()..(last_scanned_height + 1),
776 error,
777 })
778 })?;
779 }
780
781 #[cfg(feature = "orchard")]
783 {
784 let mut ironwood_subtrees = ironwood_subtrees.into_iter();
785 let mut missing_checkpoints = missing_ironwood_checkpoints.into_iter();
786 wallet_db.with_ironwood_tree_mut(|ironwood_tree| {
787 update_tree(
788 "Ironwood",
789 from_state.final_ironwood_tree(),
790 from_state.block_height(),
791 ironwood_tree,
792 anchor_retention,
793 &mut ironwood_subtrees,
794 &mut missing_checkpoints,
795 )
796 .map_err(|error| PutBlocksError::ShardTreeForBlockRange {
797 pool: ShieldedPool::Ironwood,
798 block_range: from_state.block_height()..(last_scanned_height + 1),
799 error,
800 })
801 })?;
802 }
803
804 wallet_db
805 .notify_scan_complete(
806 Range {
807 start: from_state.block_height() + 1,
808 end: last_scanned_height + 1,
809 },
810 ¬e_positions,
811 )
812 .map_err(PutBlocksError::Storage)?;
813 }
814
815 Ok(())
816}
817
818#[cfg(not(feature = "transparent-inputs"))]
819type GapError<DbT> = <DbT as LowLevelWalletRead>::Error;
820
821#[cfg(not(feature = "transparent-inputs"))]
823pub trait StoreDecryptedTxDbT: LowLevelWalletWrite {}
824
825#[cfg(not(feature = "transparent-inputs"))]
826impl<T: LowLevelWalletWrite> StoreDecryptedTxDbT for T {}
827
828#[cfg(feature = "transparent-inputs")]
829type GapError<DbT> = GapAddressesError<<DbT as LowLevelWalletRead>::Error>;
830
831#[cfg(feature = "transparent-inputs")]
839pub trait StoreDecryptedTxDbT:
840 LowLevelWalletWrite
841 + AddressStore<
842 Error = <Self as LowLevelWalletRead>::Error,
843 AccountRef = <Self as LowLevelWalletRead>::AccountRef,
844 >
845where
846 <Self as LowLevelWalletRead>::Error: From<GapError<Self>>,
847{
848}
849
850#[cfg(feature = "transparent-inputs")]
851impl<
852 T: LowLevelWalletWrite
853 + AddressStore<
854 Error = <T as LowLevelWalletRead>::Error,
855 AccountRef = <T as LowLevelWalletRead>::AccountRef,
856 >,
857> StoreDecryptedTxDbT for T
858where
859 <T as LowLevelWalletRead>::Error: From<GapError<T>>,
860{
861}
862
863pub fn store_decrypted_tx<DbT, P>(
886 wallet_db: &mut DbT,
887 params: &P,
888 #[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
889 chain_tip_height: BlockHeight,
890 d_tx: DecryptedTransaction<Transaction, <DbT as LowLevelWalletRead>::AccountId>,
891) -> Result<(), <DbT as LowLevelWalletRead>::Error>
892where
893 DbT: StoreDecryptedTxDbT,
894 <DbT as LowLevelWalletRead>::AccountId: core::fmt::Debug,
895 <DbT as LowLevelWalletRead>::Error: From<BalanceError> + From<GapError<DbT>>,
896 P: consensus::Parameters,
897{
898 let funding_accounts = wallet_db.get_funding_accounts(d_tx.tx())?;
899
900 let funding_account = funding_accounts.iter().next().copied();
902 if funding_accounts.len() > 1 {
903 warn!(
904 "More than one wallet account detected as funding transaction {:?}, selecting {:?}",
905 d_tx.tx().txid(),
906 funding_account.unwrap()
907 )
908 }
909
910 let wallet_transparent_outputs =
911 detect_wallet_transparent_outputs::<_, _, <DbT as LowLevelWalletRead>::Error>(
912 params,
913 d_tx.tx(),
914 d_tx.mined_height(),
915 funding_account,
916 #[cfg(feature = "transparent-inputs")]
917 |address| wallet_db.find_account_for_transparent_address(address),
918 )?;
919
920 if funding_account.is_none()
923 && wallet_transparent_outputs.is_empty()
924 && !d_tx.has_decrypted_outputs()
925 {
926 wallet_db.delete_retrieval_queue_entries(d_tx.tx().txid())?;
927 return Ok(());
928 }
929
930 info!("Storing decrypted transaction with id {}", d_tx.tx().txid());
931 let observed_height = d_tx.mined_height().unwrap_or(chain_tip_height + 1);
932
933 let fee = determine_fee(wallet_db, d_tx.tx())?;
936
937 let tx_ref = wallet_db.put_tx_data(d_tx.tx(), fee, None, None, observed_height)?;
938 if let Some(height) = d_tx.mined_height() {
939 wallet_db.set_transaction_status(d_tx.tx().txid(), TransactionStatus::Mined(height))?;
940 }
941
942 #[cfg(feature = "orchard")]
955 {
956 let params = PoolMigrationParams::from(AnchorRetentionInterval::default());
957 let classification = crate::data_api::zip318::classify_decrypted_tx(
958 d_tx.tx(),
959 d_tx.orchard_outputs(),
960 d_tx.ironwood_outputs(),
961 ¶ms,
962 );
963 wallet_db.put_zip318_classification(tx_ref, classification)?;
964 }
965
966 let has_wallet_shielded_spend = mark_notes_spent(
967 wallet_db,
968 tx_ref,
969 #[cfg(feature = "transparent-inputs")]
970 d_tx.tx()
971 .transparent_bundle()
972 .iter()
973 .flat_map(|b| b.vin.iter())
974 .map(|txin| txin.prevout()),
975 d_tx.tx()
976 .sapling_bundle()
977 .iter()
978 .flat_map(|b| b.shielded_spends().iter())
979 .map(|spend| spend.nullifier()),
980 #[cfg(feature = "orchard")]
981 d_tx.tx()
982 .orchard_bundle()
983 .iter()
984 .flat_map(|b| b.actions().iter())
985 .map(|action| action.nullifier()),
986 #[cfg(feature = "orchard")]
987 d_tx.tx()
988 .ironwood_bundle()
989 .iter()
990 .flat_map(|b| b.actions().iter())
991 .map(|action| action.nullifier()),
992 )?;
993
994 #[cfg(feature = "transparent-inputs")]
998 let mut tx_has_wallet_outputs = false;
999 #[cfg(feature = "transparent-inputs")]
1000 {
1001 tx_has_wallet_outputs |= !d_tx.sapling_outputs().is_empty();
1002
1003 #[cfg(feature = "orchard")]
1004 {
1005 tx_has_wallet_outputs |= !d_tx.orchard_outputs().is_empty();
1006 tx_has_wallet_outputs |= !d_tx.ironwood_outputs().is_empty();
1007 }
1008
1009 tx_has_wallet_outputs |= !wallet_transparent_outputs.is_empty();
1016 }
1017
1018 #[cfg(feature = "transparent-inputs")]
1020 let mut gap_update_set = HashSet::new();
1021
1022 put_shielded_outputs(
1023 wallet_db,
1024 Some(params),
1025 tx_ref,
1026 funding_account,
1027 d_tx.sapling_outputs(),
1028 |_, _| Ok(None),
1029 |wallet_db, output, tx_ref, spent_in| {
1030 wallet_db.put_received_sapling_note(output, tx_ref, d_tx.mined_height(), spent_in)
1031 },
1032 |_account_id| {
1033 #[cfg(feature = "transparent-inputs")]
1034 gap_update_set.insert((_account_id, TransparentKeyScope::EXTERNAL));
1035 },
1036 )?;
1037
1038 #[cfg(feature = "orchard")]
1039 put_shielded_outputs(
1040 wallet_db,
1041 Some(params),
1042 tx_ref,
1043 funding_account,
1044 d_tx.orchard_outputs(),
1045 |_, _| Ok(None),
1046 |wallet_db, output, tx_ref, spent_in| {
1047 wallet_db.put_received_orchard_note(output, tx_ref, d_tx.mined_height(), spent_in)
1048 },
1049 |_account_id| {
1050 #[cfg(feature = "transparent-inputs")]
1051 gap_update_set.insert((_account_id, TransparentKeyScope::EXTERNAL));
1052 },
1053 )?;
1054
1055 #[cfg(feature = "orchard")]
1058 put_shielded_outputs(
1059 wallet_db,
1060 Some(params),
1061 tx_ref,
1062 funding_account,
1063 d_tx.ironwood_outputs(),
1064 |_, _| Ok(None),
1065 |wallet_db, output, tx_ref, spent_in| {
1066 wallet_db.put_received_ironwood_note(output, tx_ref, d_tx.mined_height(), spent_in)
1067 },
1068 |_account_id| {
1069 #[cfg(feature = "transparent-inputs")]
1070 gap_update_set.insert((_account_id, TransparentKeyScope::EXTERNAL));
1071 },
1072 )?;
1073
1074 put_transparent_outputs(
1075 wallet_db,
1076 params,
1077 tx_ref,
1078 &wallet_transparent_outputs,
1079 #[cfg(feature = "transparent-inputs")]
1080 |wallet_db, output| wallet_db.put_transparent_output(output, observed_height, false),
1081 #[cfg(feature = "transparent-inputs")]
1082 |account_id, t_key_scope| {
1083 gap_update_set.insert((account_id, t_key_scope));
1084 },
1085 )?;
1086
1087 #[cfg(feature = "transparent-inputs")]
1089 for (account_id, key_scope) in gap_update_set {
1090 generate_transparent_gap_addresses(
1091 wallet_db,
1092 gap_limits,
1093 account_id,
1094 key_scope,
1095 UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
1096 )?;
1097 }
1098
1099 for (spending_tx_ref, spending_tx) in
1102 wallet_db.get_txs_spending_transparent_outputs_of(tx_ref)?
1103 {
1104 if let Some(fee) = determine_fee(wallet_db, &spending_tx)? {
1105 wallet_db.update_tx_fee(spending_tx_ref, fee)?;
1106 }
1107 }
1108
1109 #[cfg(feature = "transparent-inputs")]
1114 if tx_has_wallet_outputs {
1115 wallet_db.queue_transparent_input_retrieval(tx_ref, &d_tx)?
1116 }
1117
1118 wallet_db.delete_retrieval_queue_entries(d_tx.tx().txid())?;
1121
1122 if d_tx.mined_height().is_none() && !(has_wallet_shielded_spend || d_tx.has_decrypted_outputs())
1126 {
1127 wallet_db.queue_tx_status(d_tx.tx().txid())?;
1128 }
1129
1130 Ok(())
1131}
1132
1133pub(crate) fn detect_wallet_transparent_outputs<P, AccountId, E>(
1134 params: &P,
1135 tx: &Transaction,
1136 mined_height: Option<BlockHeight>,
1137 funding_account: Option<AccountId>,
1138 #[cfg(feature = "transparent-inputs")] find_account_for_address: impl Fn(
1139 &TransparentAddress,
1140 ) -> Result<
1141 Option<(AccountId, Option<TransparentKeyScope>)>,
1142 E,
1143 >,
1144) -> Result<Vec<WalletTransparentOutput<AccountId>>, E>
1145where
1146 P: consensus::Parameters,
1147 AccountId: Copy + core::fmt::Debug + std::hash::Hash + std::cmp::Eq,
1148{
1149 let mut result = vec![];
1150 for (output_index, txout) in tx
1151 .transparent_bundle()
1152 .iter()
1153 .flat_map(|b| b.vout.iter())
1154 .enumerate()
1155 {
1156 let script_kind = txout.script_kind();
1157 if let Some(address) = script_kind
1158 .as_ref()
1159 .and_then(TransparentAddress::from_script_kind)
1160 {
1161 debug!(
1162 "{:?} output {} has recipient {}",
1163 tx.txid(),
1164 output_index,
1165 address.encode(params)
1166 );
1167
1168 #[allow(unused_mut)]
1169 let mut detected = false;
1170
1171 #[cfg(feature = "transparent-inputs")]
1173 if let Some((account_uuid, key_scope)) = find_account_for_address(&address)? {
1174 debug!(
1175 "{:?} output {} belongs to account {:?}",
1176 tx.txid(),
1177 output_index,
1178 account_uuid
1179 );
1180 result.push(
1181 WalletTransparentOutput::from_parts(
1182 OutPoint::new(tx.txid().into(), u32::try_from(output_index).unwrap()),
1183 txout.clone(),
1184 mined_height,
1185 Some(account_uuid),
1186 key_scope,
1187 funding_account,
1188 )
1189 .expect("txout.recipient_address extraction previously checked"),
1190 );
1191 detected = true;
1192 } else {
1193 debug!(
1194 "Address {} is not recognized as belonging to any of our accounts.",
1195 address.encode(params)
1196 );
1197 }
1198
1199 if !detected {
1200 if let Some(account_id) = funding_account {
1204 result.push(
1205 WalletTransparentOutput::from_parts(
1206 OutPoint::new(tx.txid().into(), u32::try_from(output_index).unwrap()),
1207 txout.clone(),
1208 mined_height,
1209 None,
1210 None,
1211 Some(account_id),
1212 )
1213 .expect("txout.recipient_address extraction previously checked"),
1214 );
1215 }
1216 }
1217 } else if let Some(script_kind) = script_kind {
1218 if !matches!(script_kind, ScriptKind::NullData { .. }) {
1222 warn!(
1223 "Ignoring unsupported script kind '{}' for tx {} output {}",
1224 script_kind.as_str(),
1225 tx.txid(),
1226 output_index
1227 );
1228 }
1229 } else {
1230 warn!(
1231 "Unable to determine recipient address for tx {} output {}",
1232 tx.txid(),
1233 output_index
1234 );
1235 }
1236 }
1237
1238 Ok(result)
1239}
1240
1241fn mark_notes_spent<'a, DbT>(
1242 wallet_db: &mut DbT,
1243 tx_ref: <DbT as LowLevelWalletRead>::TxRef,
1244 #[cfg(feature = "transparent-inputs")] transparent_prevouts: impl Iterator<
1245 Item = &'a transparent::bundle::OutPoint,
1246 >,
1247 sapling_nfs: impl Iterator<Item = &'a sapling::Nullifier>,
1248 #[cfg(feature = "orchard")] orchard_nfs: impl Iterator<Item = &'a orchard::note::Nullifier>,
1249 #[cfg(feature = "orchard")] ironwood_nfs: impl Iterator<Item = &'a orchard::note::Nullifier>,
1250) -> Result<bool, <DbT as LowLevelWalletRead>::Error>
1251where
1252 DbT: LowLevelWalletWrite,
1253{
1254 let mut has_wallet_shielded_spend = false;
1255
1256 #[cfg(feature = "transparent-inputs")]
1258 for outpoint in transparent_prevouts {
1259 wallet_db.mark_transparent_utxo_spent(outpoint, tx_ref)?;
1260 }
1261
1262 for nf in sapling_nfs {
1264 has_wallet_shielded_spend |= wallet_db.mark_sapling_note_spent(nf, tx_ref)?;
1265 }
1266
1267 #[cfg(feature = "orchard")]
1269 for nf in orchard_nfs {
1270 has_wallet_shielded_spend |= wallet_db.mark_orchard_note_spent(nf, tx_ref)?;
1271 }
1272
1273 #[cfg(feature = "orchard")]
1275 for nf in ironwood_nfs {
1276 has_wallet_shielded_spend |= wallet_db.mark_ironwood_note_spent(nf, tx_ref)?;
1277 }
1278
1279 Ok(has_wallet_shielded_spend)
1280}
1281
1282#[allow(clippy::too_many_arguments)]
1283fn put_shielded_outputs<DbT, P, Output>(
1284 wallet_db: &mut DbT,
1285 params: Option<&P>,
1286 tx_ref: <DbT as LowLevelWalletRead>::TxRef,
1287 funding_account: Option<DbT::AccountId>,
1288 outputs: &[Output],
1289 detect_note_spent_in: impl Fn(
1290 &mut DbT,
1291 &Output,
1292 ) -> Result<
1293 Option<<DbT as LowLevelWalletRead>::TxRef>,
1294 <DbT as LowLevelWalletRead>::Error,
1295 >,
1296 put_received_note: impl Fn(
1297 &mut DbT,
1298 &Output,
1299 <DbT as LowLevelWalletRead>::TxRef,
1300 Option<<DbT as LowLevelWalletRead>::TxRef>,
1301 ) -> Result<(), <DbT as LowLevelWalletRead>::Error>,
1302 mut on_external_account: impl FnMut(<DbT as LowLevelWalletRead>::AccountId),
1303) -> Result<(), <DbT as LowLevelWalletRead>::Error>
1304where
1305 DbT: LowLevelWalletWrite,
1306 P: consensus::Parameters,
1307 Output: ReceivedShieldedOutput<AccountId = <DbT as LowLevelWalletRead>::AccountId>,
1308{
1309 for output in outputs {
1310 let sent_output = match output.transfer_type() {
1311 TransferType::Outgoing => {
1312 let note = output.to_wallet_note();
1313
1314 let recipient = Recipient::External {
1315 recipient_address: external_address(
1316 wallet_db,
1317 params.expect("present when outgoing is possible (store_decrypted_tx)"),
1318 output.account_id(),
1319 note.receiver(),
1320 )?,
1321 output_pool: PoolType::Shielded(note.pool()),
1322 };
1323
1324 Some((output.account_id(), recipient, note.value()))
1325 }
1326 TransferType::AccountInternal => {
1327 let spent_in = detect_note_spent_in(wallet_db, output)?;
1328 put_received_note(wallet_db, output, tx_ref, spent_in)?;
1329
1330 let note = output.to_wallet_note();
1331 let value = note.value();
1332
1333 let recipient = Recipient::InternalShielded {
1334 receiving_account: output.account_id(),
1335 external_address: None,
1336 note: Box::new(note),
1337 };
1338
1339 Some((output.account_id(), recipient, value))
1340 }
1341 TransferType::Incoming => {
1342 let spent_in = detect_note_spent_in(wallet_db, output)?;
1343 put_received_note(wallet_db, output, tx_ref, spent_in)?;
1344 on_external_account(output.account_id());
1345
1346 if let Some(account_id) = funding_account {
1347 let note = output.to_wallet_note();
1348 let value = note.value();
1349
1350 let recipient = Recipient::InternalShielded {
1352 receiving_account: output.account_id(),
1353 external_address: Some(external_address(
1354 wallet_db,
1355 params.expect(
1356 "present when funding_account is known (store_decrypted_tx)",
1357 ),
1358 output.account_id(),
1359 note.receiver(),
1360 )?),
1361 note: Box::new(note),
1362 };
1363
1364 Some((account_id, recipient, value))
1365 } else {
1366 None
1367 }
1368 }
1369 TransferType::WalletInternal => unreachable!(
1370 "TransferType::WalletInternal is only produced for transparent outputs"
1371 ),
1372 };
1373
1374 if let Some((from_account_uuid, recipient, value)) = sent_output {
1375 wallet_db.put_sent_output(
1376 from_account_uuid,
1377 tx_ref,
1378 output.index(),
1379 &recipient,
1380 value,
1381 output.memo(),
1382 )?;
1383 }
1384 }
1385
1386 Ok(())
1387}
1388
1389fn put_transparent_outputs<DbT, P>(
1390 wallet_db: &mut DbT,
1391 params: &P,
1392 tx_ref: <DbT as LowLevelWalletRead>::TxRef,
1393 outputs: &[WalletTransparentOutput<<DbT as LowLevelWalletRead>::AccountId>],
1394 #[cfg(feature = "transparent-inputs")] put_received_output: impl Fn(
1395 &mut DbT,
1396 &WalletTransparentOutput<<DbT as LowLevelWalletRead>::AccountId>,
1397 ) -> Result<
1398 (
1399 <DbT as LowLevelWalletRead>::AccountId,
1400 std::option::Option<TransparentKeyScope>,
1401 ),
1402 <DbT as LowLevelWalletRead>::Error,
1403 >,
1404 #[cfg(feature = "transparent-inputs")] mut on_received: impl FnMut(
1405 <DbT as LowLevelWalletRead>::AccountId,
1406 TransparentKeyScope,
1407 ),
1408) -> Result<(), <DbT as LowLevelWalletRead>::Error>
1409where
1410 DbT: LowLevelWalletWrite,
1411 P: consensus::Parameters,
1412{
1413 for output in outputs {
1414 #[cfg(feature = "transparent-inputs")]
1417 if output.recipient_account().is_some() {
1418 let (account_id, _) = put_received_output(wallet_db, output)?;
1419
1420 if let Some(t_key_scope) = output.recipient_key_scope() {
1421 on_received(account_id, t_key_scope);
1422 }
1423
1424 wallet_db.queue_transparent_spend_detection(
1438 *output.recipient_address(),
1439 tx_ref,
1440 output.outpoint().n(),
1441 )?;
1442 }
1443
1444 if let Some(&from_account) = output.funding_account() {
1448 let recipient = match output.recipient_account() {
1449 #[cfg(feature = "transparent-inputs")]
1450 Some(&receiving_account) => Recipient::InternalTransparent {
1451 receiving_account,
1452 recipient_address: *output.recipient_address(),
1453 },
1454 #[cfg(not(feature = "transparent-inputs"))]
1455 Some(_) => Recipient::External {
1456 recipient_address: Receiver::Transparent(*output.recipient_address())
1457 .to_zcash_address(params.network_type()),
1458 output_pool: PoolType::TRANSPARENT,
1459 },
1460 None => {
1461 let receiver = Receiver::Transparent(*output.recipient_address());
1462
1463 #[cfg(feature = "transparent-inputs")]
1464 let recipient_address =
1465 external_address(wallet_db, params, from_account, receiver)?;
1466
1467 #[cfg(not(feature = "transparent-inputs"))]
1468 let recipient_address = receiver.to_zcash_address(params.network_type());
1469
1470 Recipient::External {
1471 recipient_address,
1472 output_pool: PoolType::TRANSPARENT,
1473 }
1474 }
1475 };
1476
1477 wallet_db.put_sent_output(
1478 from_account,
1479 tx_ref,
1480 output.index(),
1481 &recipient,
1482 output.value(),
1483 None,
1484 )?;
1485 }
1486 }
1487
1488 Ok(())
1489}
1490
1491fn external_address<DbT, P>(
1493 wallet_db: &DbT,
1494 params: &P,
1495 account_id: DbT::AccountId,
1496 receiver: Receiver,
1497) -> Result<zcash_address::ZcashAddress, <DbT as LowLevelWalletRead>::Error>
1498where
1499 DbT: LowLevelWalletRead,
1500 P: consensus::Parameters,
1501{
1502 let recipient_address = wallet_db
1503 .select_receiving_address(account_id, &receiver)?
1504 .unwrap_or_else(|| receiver.to_zcash_address(params.network_type()));
1505
1506 Ok(recipient_address)
1507}
1508
1509pub fn build_subtrees<H, const SHARD_HEIGHT: u8>(
1518 start_position: Position,
1519 commitments: &mut [Option<(H, Retention<BlockHeight>)>],
1520 chunk_size: usize,
1521) -> Vec<(LocatedPrunableTree<H>, BTreeMap<BlockHeight, Position>)>
1522where
1523 H: Clone + PartialEq + Hashable + Send + Sync,
1524{
1525 commitments
1526 .par_chunks_mut(chunk_size)
1527 .enumerate()
1528 .filter_map(|(i, chunk)| {
1529 let start = start_position + (i * chunk_size) as u64;
1530 let end = start + chunk.len() as u64;
1531
1532 shardtree::LocatedTree::from_iter(
1533 start..end,
1534 SHARD_HEIGHT.into(),
1535 chunk.iter_mut().map(|n| n.take().expect("always Some")),
1536 )
1537 })
1538 .map(|res| (res.subtree, res.checkpoints))
1539 .collect()
1540}
1541
1542#[cfg(feature = "orchard")]
1544pub fn checkpoint_positions<H>(
1545 subtrees: &[(LocatedPrunableTree<H>, BTreeMap<BlockHeight, Position>)],
1546) -> BTreeMap<BlockHeight, Position> {
1547 subtrees
1548 .iter()
1549 .flat_map(|(_, checkpoints)| checkpoints.iter())
1550 .map(|(k, v)| (*k, *v))
1551 .collect()
1552}
1553
1554#[cfg(feature = "orchard")]
1559pub fn ensure_checkpoints<'a, H, I: Iterator<Item = &'a BlockHeight>, const DEPTH: u8>(
1560 ensure_heights: I,
1563 existing_checkpoint_positions: &BTreeMap<BlockHeight, Position>,
1566 state_final_tree: &Frontier<H, DEPTH>,
1569) -> Vec<(BlockHeight, Checkpoint)> {
1570 ensure_heights
1571 .flat_map(|ensure_height| {
1572 existing_checkpoint_positions
1573 .range::<BlockHeight, _>(..=*ensure_height)
1574 .last()
1575 .map_or_else(
1576 || {
1577 Some((
1578 *ensure_height,
1579 state_final_tree
1580 .value()
1581 .map_or_else(Checkpoint::tree_empty, |t| {
1582 Checkpoint::at_position(t.position())
1583 }),
1584 ))
1585 },
1586 |(existing_checkpoint_height, position)| {
1587 if *existing_checkpoint_height < *ensure_height {
1588 Some((*ensure_height, Checkpoint::at_position(*position)))
1589 } else {
1590 None
1593 }
1594 },
1595 )
1596 .into_iter()
1597 })
1598 .collect::<Vec<_>>()
1599}
1600
1601pub const NULLIFIER_MAP_RETENTION_BLOCKS: u32 = 100;
1609
1610fn nullifier_tracking_floor(
1618 fully_scanned: Option<BlockHeight>,
1619 from_state_height: BlockHeight,
1620 batch_end: Option<BlockHeight>,
1621) -> Option<BlockHeight> {
1622 if fully_scanned == Some(from_state_height) {
1623 batch_end.and_then(|last| {
1624 let floor =
1625 BlockHeight::from(u32::from(last).saturating_sub(NULLIFIER_MAP_RETENTION_BLOCKS));
1626 (floor > from_state_height + 1).then_some(floor)
1627 })
1628 } else {
1629 None
1630 }
1631}
1632
1633fn should_track_nullifiers(
1640 nullifier_tracking_floor: Option<BlockHeight>,
1641 block_height: BlockHeight,
1642) -> bool {
1643 nullifier_tracking_floor.is_none_or(|floor| block_height >= floor)
1644}
1645
1646fn should_retain_anchor(anchor_retention: Option<&AnchorRetention>, height: BlockHeight) -> bool {
1650 anchor_retention.is_some_and(|retention| retention.retains(height))
1651}
1652
1653fn retain_anchor_checkpoint<S, const DEPTH: u8, const SHARD_HEIGHT: u8>(
1655 tree: &mut ShardTree<S, DEPTH, SHARD_HEIGHT>,
1656 anchor_retention: Option<&AnchorRetention>,
1657 height: BlockHeight,
1658) -> Result<(), ShardTreeError<S::Error>>
1659where
1660 S: ShardStore<CheckpointId = BlockHeight>,
1661 S::H: Clone + PartialEq + Hashable,
1662{
1663 if should_retain_anchor(anchor_retention, height) {
1664 tree.ensure_retained(height)?;
1665 }
1666 Ok(())
1667}
1668
1669#[cfg(feature = "orchard")]
1680pub fn cross_pool_ensure_heights(
1681 sapling: &BTreeSet<BlockHeight>,
1682 orchard: &BTreeSet<BlockHeight>,
1683 ironwood: &BTreeSet<BlockHeight>,
1684) -> [BTreeSet<BlockHeight>; 3] {
1685 let union = |a: &BTreeSet<BlockHeight>, b: &BTreeSet<BlockHeight>| {
1686 a.union(b).copied().collect::<BTreeSet<BlockHeight>>()
1687 };
1688 [
1689 union(orchard, ironwood),
1690 union(sapling, ironwood),
1691 union(sapling, orchard),
1692 ]
1693}
1694
1695#[cfg(feature = "orchard")]
1719pub fn batch_ensure_heights(
1720 sapling: &BTreeSet<BlockHeight>,
1721 orchard: &BTreeSet<BlockHeight>,
1722 ironwood: &BTreeSet<BlockHeight>,
1723 anchor_retention: Option<&AnchorRetention>,
1724 range: std::ops::RangeInclusive<BlockHeight>,
1725) -> [BTreeSet<BlockHeight>; 3] {
1726 let mut ensure = cross_pool_ensure_heights(sapling, orchard, ironwood);
1727
1728 if let Some(retention) = anchor_retention {
1729 let retained = retention.retained_in_range(range);
1730 for pool in ensure.iter_mut() {
1731 pool.extend(retained.iter().copied());
1732 }
1733 }
1734
1735 ensure
1736}
1737
1738pub fn update_tree<S, const DEPTH: u8, const SHARD_HEIGHT: u8>(
1748 protocol: &'static str,
1749 frontier: &Frontier<S::H, DEPTH>,
1750 frontier_height: BlockHeight,
1751 tree: &mut ShardTree<S, DEPTH, SHARD_HEIGHT>,
1752 anchor_retention: Option<&AnchorRetention>,
1753 subtrees: impl Iterator<Item = (LocatedPrunableTree<S::H>, BTreeMap<BlockHeight, Position>)>,
1754 #[cfg(feature = "orchard")] missing_checkpoints: impl Iterator<Item = (BlockHeight, Checkpoint)>,
1755) -> Result<(), ShardTreeError<S::Error>>
1756where
1757 S: ShardStore<CheckpointId = BlockHeight>,
1758 S::H: Clone + PartialEq + Hashable,
1759{
1760 debug!(
1761 "{protocol} initial tree size at {frontier_height:?}: {:?}",
1762 frontier.tree_size()
1763 );
1764 tree.insert_frontier(
1767 frontier.clone(),
1768 Retention::Checkpoint {
1769 id: frontier_height,
1770 marking: Marking::Reference,
1771 },
1772 )?;
1773 retain_anchor_checkpoint(tree, anchor_retention, frontier_height)?;
1774
1775 for (subtree, checkpoints) in subtrees {
1776 for height in checkpoints.keys() {
1782 retain_anchor_checkpoint(tree, anchor_retention, *height)?;
1783 }
1784 tree.insert_tree(subtree, checkpoints)?;
1785 }
1786
1787 #[cfg(feature = "orchard")]
1791 {
1792 let min_checkpoint_height = tree
1793 .store()
1794 .min_checkpoint_id()
1795 .map_err(ShardTreeError::Storage)?
1796 .expect("At least one checkpoint was inserted (by insert_frontier)");
1797
1798 for (height, checkpoint) in missing_checkpoints {
1799 if height > min_checkpoint_height {
1800 debug!(
1801 "Adding missing {protocol} checkpoint for height: {:?}: {:?}",
1802 height,
1803 checkpoint.position()
1804 );
1805 tree.store_mut()
1806 .add_checkpoint(height, checkpoint.clone())
1807 .map_err(ShardTreeError::Storage)?;
1808 retain_anchor_checkpoint(tree, anchor_retention, height)?;
1809 }
1810 }
1811 }
1812
1813 Ok(())
1814}
1815
1816#[cfg(test)]
1817mod tests {
1818 #[cfg(feature = "orchard")]
1819 use {super::cross_pool_ensure_heights, std::collections::BTreeSet};
1820
1821 use core::num::NonZeroU32;
1822
1823 use proptest::prelude::*;
1824 use zcash_protocol::consensus::BlockHeight;
1825
1826 #[cfg(feature = "orchard")]
1827 use super::batch_ensure_heights;
1828 use super::{
1829 NULLIFIER_MAP_RETENTION_BLOCKS, nullifier_tracking_floor, should_retain_anchor,
1830 should_track_nullifiers,
1831 };
1832 use crate::data_api::anchor_retention::{AnchorRetention, AnchorRetentionInterval};
1833
1834 #[test]
1838 fn out_of_order_ranges_track_fully() {
1839 let h = BlockHeight::from;
1840 assert_eq!(
1842 nullifier_tracking_floor(Some(h(1_000)), h(500_000), Some(h(510_000))),
1843 None
1844 );
1845 assert_eq!(
1847 nullifier_tracking_floor(None, h(500_000), Some(h(510_000))),
1848 None
1849 );
1850 assert_eq!(
1852 nullifier_tracking_floor(Some(h(600_000)), h(500_000), Some(h(510_000))),
1853 None
1854 );
1855 }
1856
1857 #[test]
1861 fn frontier_batches_retain_the_trailing_window() {
1862 let from = BlockHeight::from(500_000);
1863 let last = BlockHeight::from(510_000);
1864 let floor =
1865 nullifier_tracking_floor(Some(from), from, Some(last)).expect("frontier ⇒ floor");
1866 assert_eq!(
1867 u32::from(last) - u32::from(floor),
1868 NULLIFIER_MAP_RETENTION_BLOCKS
1869 );
1870
1871 let short = BlockHeight::from(500_000 + NULLIFIER_MAP_RETENTION_BLOCKS / 2);
1872 assert_eq!(
1873 nullifier_tracking_floor(Some(from), from, Some(short)),
1874 None
1875 );
1876 assert_eq!(nullifier_tracking_floor(Some(from), from, None), None);
1877 }
1878
1879 #[test]
1880 fn nullifier_tracking_floor_gating() {
1881 let floor = BlockHeight::from(1000);
1882
1883 assert!(should_track_nullifiers(None, BlockHeight::from(0)));
1885 assert!(should_track_nullifiers(None, BlockHeight::from(999)));
1886
1887 assert!(should_track_nullifiers(
1889 Some(floor),
1890 BlockHeight::from(1000)
1891 ));
1892 assert!(should_track_nullifiers(
1893 Some(floor),
1894 BlockHeight::from(1001)
1895 ));
1896
1897 assert!(!should_track_nullifiers(
1899 Some(floor),
1900 BlockHeight::from(999)
1901 ));
1902 assert!(!should_track_nullifiers(Some(floor), BlockHeight::from(0)));
1903 }
1904
1905 #[test]
1908 fn anchor_retention_gating() {
1909 for interval in [
1910 AnchorRetentionInterval::ZIP_318,
1911 AnchorRetentionInterval::custom(NonZeroU32::new(12).expect("nonzero")),
1912 ] {
1913 let blocks = interval.block_count().get();
1914 let floor = BlockHeight::from(4 * blocks);
1915 let policy = AnchorRetention::new(floor, interval);
1916 let retention = Some(&policy);
1917
1918 assert!(!should_retain_anchor(None, BlockHeight::from(8 * blocks)));
1920
1921 assert!(should_retain_anchor(
1923 retention,
1924 BlockHeight::from(4 * blocks)
1925 ));
1926 assert!(should_retain_anchor(
1927 retention,
1928 BlockHeight::from(8 * blocks)
1929 ));
1930
1931 assert!(!should_retain_anchor(
1933 retention,
1934 BlockHeight::from(3 * blocks)
1935 ));
1936
1937 assert!(!should_retain_anchor(
1939 retention,
1940 BlockHeight::from(4 * blocks + 1)
1941 ));
1942 assert!(!should_retain_anchor(
1943 retention,
1944 BlockHeight::from(5 * blocks - 1)
1945 ));
1946 }
1947 }
1948
1949 #[cfg(feature = "orchard")]
1950 prop_compose! {
1951 fn arb_heights()(
1953 heights in proptest::collection::vec(0u32..100, 0..20),
1954 ) -> BTreeSet<BlockHeight> {
1955 heights.into_iter().map(BlockHeight::from).collect()
1956 }
1957 }
1958
1959 #[cfg(feature = "orchard")]
1960 fn union(a: &BTreeSet<BlockHeight>, b: &BTreeSet<BlockHeight>) -> BTreeSet<BlockHeight> {
1961 a.union(b).copied().collect()
1962 }
1963
1964 proptest! {
1965 #[test]
1970 #[cfg(feature = "orchard")]
1971 fn ensure_heights_align_all_pools(
1972 sapling in arb_heights(),
1973 orchard in arb_heights(),
1974 ironwood in arb_heights(),
1975 ) {
1976 let [ensure_sapling, ensure_orchard, ensure_ironwood] =
1977 cross_pool_ensure_heights(&sapling, &orchard, &ironwood);
1978
1979 let total = union(&union(&sapling, &orchard), &ironwood);
1980
1981 prop_assert_eq!(union(&sapling, &ensure_sapling), total.clone());
1982 prop_assert_eq!(union(&orchard, &ensure_orchard), total.clone());
1983 prop_assert_eq!(union(&ironwood, &ensure_ironwood), total);
1984
1985 prop_assert_eq!(ensure_sapling, union(&orchard, &ironwood));
1988 prop_assert_eq!(ensure_orchard, union(&sapling, &ironwood));
1989 prop_assert_eq!(ensure_ironwood, union(&sapling, &orchard));
1990 }
1991
1992 #[test]
1996 #[cfg(feature = "orchard")]
1997 fn ensure_heights_degrade_to_two_pools(
1998 sapling in arb_heights(),
1999 orchard in arb_heights(),
2000 ) {
2001 let [ensure_sapling, ensure_orchard, ensure_ironwood] =
2002 cross_pool_ensure_heights(&sapling, &orchard, &BTreeSet::new());
2003
2004 prop_assert_eq!(ensure_sapling, orchard.clone());
2005 prop_assert_eq!(ensure_orchard, sapling.clone());
2006 prop_assert_eq!(ensure_ironwood, union(&sapling, &orchard));
2007 }
2008 }
2009
2010 #[cfg(feature = "orchard")]
2019 #[test]
2020 fn retained_boundary_on_a_commitment_free_block_is_ensured() {
2021 let h = BlockHeight::from;
2022 let retention = AnchorRetention::new(
2025 h(1_000),
2026 AnchorRetentionInterval::custom(NonZeroU32::new(12).unwrap()),
2027 );
2028
2029 let (sap_cp, orch_cp, iw_cp) = (
2030 BTreeSet::from([h(1_198)]),
2031 BTreeSet::from([h(1_205)]),
2032 BTreeSet::new(),
2033 );
2034
2035 for heights in cross_pool_ensure_heights(&sap_cp, &orch_cp, &iw_cp) {
2039 assert!(
2040 !heights.contains(&h(1_200)),
2041 "cross-pool alignment must not supply the boundary; the union is what does"
2042 );
2043 }
2044
2045 let [sapling, orchard, ironwood] = batch_ensure_heights(
2046 &sap_cp,
2047 &orch_cp,
2048 &iw_cp,
2049 Some(&retention),
2050 h(1_150)..=h(1_250),
2051 );
2052
2053 for (pool, heights) in [
2054 ("sapling", &sapling),
2055 ("orchard", &orchard),
2056 ("ironwood", &ironwood),
2057 ] {
2058 assert!(
2059 heights.contains(&h(1_200)),
2060 "{pool} must ensure the retained boundary 1200, got {heights:?}"
2061 );
2062 }
2063 }
2064
2065 #[cfg(feature = "orchard")]
2069 #[test]
2070 fn no_retention_policy_is_exactly_cross_pool() {
2071 let h = BlockHeight::from;
2072 let sapling = BTreeSet::from([h(100), h(140)]);
2073 let orchard = BTreeSet::from([h(120)]);
2074 let ironwood = BTreeSet::from([h(160)]);
2075
2076 assert_eq!(
2077 batch_ensure_heights(&sapling, &orchard, &ironwood, None, h(1)..=h(1_000)),
2078 cross_pool_ensure_heights(&sapling, &orchard, &ironwood)
2079 );
2080 }
2081}