Skip to main content

pepper_sync/wallet/
traits.rs

1//! Traits for interfacing a wallet with the sync engine
2
3use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::ops::{Range, RangeInclusive};
5
6use tokio::sync::mpsc;
7use zip32::DiversifierIndex;
8
9use incrementalmerkletree::Hashable;
10use orchard::tree::MerkleHashOrchard;
11use shardtree::ShardTree;
12use shardtree::store::memory::MemoryShardStore;
13use shardtree::store::{Checkpoint, ShardStore, TreeState};
14use zcash_client_backend::data_api::anchor_retention::AnchorRetention;
15use zcash_keys::keys::UnifiedFullViewingKey;
16use zcash_primitives::transaction::TxId;
17use zcash_protocol::consensus::{self, BlockHeight};
18use zcash_protocol::{PoolType, ShieldedPool};
19use zip32::AccountId;
20
21use crate::error::{ServerError, SyncError};
22use crate::keys::transparent::TransparentAddressId;
23use crate::reset_spends;
24use crate::shardtree_ext::{RollbackOutcome, ShardTreeExt};
25use crate::sync::truncate::{PoolTruncation, plan_pool_truncation, tree_facts};
26use crate::sync::{MAX_SHARDTREE_CHECKPOINTS, ScanRange};
27use crate::wallet::{
28    Ironwood, NullifierMap, Orchard, OutputId, Sapling, ShardTrees, SyncState, WalletBlock,
29    WalletTransaction, empty_shard_tree,
30};
31use crate::witness::LocatedTreeData;
32use crate::{SyncDomain, client, sync::set_transactions_failed_unchecked};
33
34use super::{FetchRequest, ScanTarget, witness};
35
36/// Trait for interfacing wallet with the sync engine.
37pub trait SyncWallet {
38    /// Errors associated with interfacing the sync engine with wallet data
39    type Error: std::fmt::Debug + std::fmt::Display + std::error::Error;
40
41    /// Returns the block height wallet was created.
42    fn get_birthday(&self) -> Result<BlockHeight, Self::Error>;
43
44    /// Returns a reference to wallet sync state.
45    fn get_sync_state(&self) -> Result<&SyncState, Self::Error>;
46
47    /// Returns a mutable reference to wallet sync state.
48    fn get_sync_state_mut(&mut self) -> Result<&mut SyncState, Self::Error>;
49
50    /// Returns all unified full viewing keys known to this wallet.
51    fn get_unified_full_viewing_keys(
52        &self,
53    ) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error>;
54
55    /// Add orchard address to wallet's unified address list.
56    fn add_orchard_address(
57        &mut self,
58        account_id: zip32::AccountId,
59        address: orchard::Address,
60        diversifier_index: DiversifierIndex,
61    ) -> Result<(), Self::Error>;
62
63    /// Add sapling address to wallet's unified address list.
64    fn add_sapling_address(
65        &mut self,
66        account_id: zip32::AccountId,
67        address: sapling_crypto::PaymentAddress,
68        diversifier_index: DiversifierIndex,
69    ) -> Result<(), Self::Error>;
70
71    /// Returns a reference to all transparent addresses known to this wallet.
72    ///
73    /// These addresses must be in-use, they do not include gap addresses.
74    fn get_transparent_addresses(
75        &self,
76    ) -> Result<&BTreeMap<TransparentAddressId, String>, Self::Error>;
77
78    /// Returns a mutable reference to all transparent addresses known to this wallet.
79    ///
80    /// These addresses must be in-use, they do not include gap addresses.
81    fn get_transparent_addresses_mut(
82        &mut self,
83    ) -> Result<&mut BTreeMap<TransparentAddressId, String>, Self::Error>;
84
85    /// Aids in-memory wallets to only save when the wallet state has changed by setting a flag to mark that save is
86    /// required.
87    /// Persitance wallets may use the default implementation.
88    fn set_save_flag(&mut self) -> Result<(), Self::Error> {
89        Ok(())
90    }
91}
92
93/// Trait for interfacing [`crate::wallet::WalletBlock`]s with wallet data
94pub trait SyncBlocks: SyncWallet {
95    /// Get a stored wallet compact block from wallet data by block height
96    ///
97    /// Must return error if block is not found
98    fn get_wallet_block(&self, block_height: BlockHeight) -> Result<WalletBlock, Self::Error>;
99
100    /// Get mutable reference to wallet blocks
101    fn get_wallet_blocks_mut(
102        &mut self,
103    ) -> Result<&mut BTreeMap<BlockHeight, WalletBlock>, Self::Error>;
104
105    /// Append wallet compact blocks to wallet data
106    fn append_wallet_blocks(
107        &mut self,
108        mut wallet_blocks: BTreeMap<BlockHeight, WalletBlock>,
109    ) -> Result<(), Self::Error> {
110        self.get_wallet_blocks_mut()?.append(&mut wallet_blocks);
111
112        Ok(())
113    }
114
115    /// Removes all wallet blocks above the given `block_height`.
116    fn truncate_wallet_blocks(&mut self, truncate_height: BlockHeight) -> Result<(), Self::Error> {
117        self.get_wallet_blocks_mut()?
118            .retain(|block_height, _| *block_height <= truncate_height);
119
120        Ok(())
121    }
122}
123
124/// Trait for interfacing [`crate::wallet::WalletTransaction`]s with wallet data
125pub trait SyncTransactions: SyncWallet {
126    /// Get reference to wallet transactions
127    fn get_wallet_transactions(&self) -> Result<&HashMap<TxId, WalletTransaction>, Self::Error>;
128
129    /// Get mutable reference to wallet transactions
130    fn get_wallet_transactions_mut(
131        &mut self,
132    ) -> Result<&mut HashMap<TxId, WalletTransaction>, Self::Error>;
133
134    /// Insert wallet transaction
135    fn insert_wallet_transaction(
136        &mut self,
137        wallet_transaction: WalletTransaction,
138    ) -> Result<(), Self::Error> {
139        self.get_wallet_transactions_mut()?
140            .insert(wallet_transaction.txid(), wallet_transaction);
141
142        Ok(())
143    }
144
145    /// Extend wallet transaction map with new wallet transactions.
146    ///
147    /// A transaction in `wallet_transactions` must replace the wallet's record of the transaction with the same txid.
148    fn extend_wallet_transactions(
149        &mut self,
150        wallet_transactions: HashMap<TxId, WalletTransaction>,
151    ) -> Result<(), Self::Error> {
152        self.get_wallet_transactions_mut()?
153            .extend(wallet_transactions);
154
155        Ok(())
156    }
157
158    /// Sets all confirmed wallet transactions above the given `block_height` to `Failed` status.
159    /// Also sets any output's `spending_transaction` field to `None` if it's spending transaction was set to `Failed`
160    /// status.
161    fn truncate_wallet_transactions(
162        &mut self,
163        truncate_height: BlockHeight,
164        set_truncated_transactions_failed: bool,
165    ) -> Result<(), Self::Error> {
166        let invalid_txids: Vec<TxId> = self
167            .get_wallet_transactions()?
168            .values()
169            .filter(|tx| tx.status().is_confirmed_after(&truncate_height))
170            .map(|tx| tx.transaction().txid())
171            .collect();
172
173        if set_truncated_transactions_failed {
174            set_transactions_failed_unchecked(self.get_wallet_transactions_mut()?, invalid_txids);
175        } else {
176            let wallet_transactions = self.get_wallet_transactions_mut()?;
177            wallet_transactions.retain(|txid, _| !invalid_txids.contains(txid));
178            reset_spends(wallet_transactions, invalid_txids);
179        }
180
181        Ok(())
182    }
183}
184
185/// Trait for interfacing nullifiers with wallet data
186pub trait SyncNullifiers: SyncWallet {
187    /// Get wallet nullifier map
188    fn get_nullifiers(&self) -> Result<&NullifierMap, Self::Error>;
189
190    /// Get mutable reference to wallet nullifier map
191    fn get_nullifiers_mut(&mut self) -> Result<&mut NullifierMap, Self::Error>;
192
193    /// Append nullifiers to wallet nullifier map
194    fn append_nullifiers(&mut self, nullifiers: &mut NullifierMap) -> Result<(), Self::Error> {
195        self.get_nullifiers_mut()?
196            .sapling
197            .append(&mut nullifiers.sapling);
198        self.get_nullifiers_mut()?
199            .orchard
200            .append(&mut nullifiers.orchard);
201        self.get_nullifiers_mut()?
202            .ironwood
203            .append(&mut nullifiers.ironwood);
204
205        Ok(())
206    }
207
208    /// Removes all mapped nullifiers above the given `block_height`.
209    fn truncate_nullifiers(&mut self, truncate_height: BlockHeight) -> Result<(), Self::Error> {
210        let nullifier_map = self.get_nullifiers_mut()?;
211        nullifier_map
212            .sapling
213            .retain(|_, scan_target| scan_target.block_height() <= truncate_height);
214        nullifier_map
215            .orchard
216            .retain(|_, scan_target| scan_target.block_height() <= truncate_height);
217        nullifier_map
218            .ironwood
219            .retain(|_, scan_target| scan_target.block_height() <= truncate_height);
220
221        Ok(())
222    }
223}
224
225/// Trait for interfacing outpoints with wallet data
226pub trait SyncOutPoints: SyncWallet {
227    /// Get wallet outpoint map
228    fn get_outpoints(&self) -> Result<&BTreeMap<OutputId, ScanTarget>, Self::Error>;
229
230    /// Get mutable reference to wallet outpoint map
231    fn get_outpoints_mut(&mut self) -> Result<&mut BTreeMap<OutputId, ScanTarget>, Self::Error>;
232
233    /// Append outpoints to wallet outpoint map
234    fn append_outpoints(
235        &mut self,
236        outpoints: &mut BTreeMap<OutputId, ScanTarget>,
237    ) -> Result<(), Self::Error> {
238        self.get_outpoints_mut()?.append(outpoints);
239
240        Ok(())
241    }
242
243    /// Removes all mapped outpoints above the given `block_height`.
244    fn truncate_outpoints(&mut self, truncate_height: BlockHeight) -> Result<(), Self::Error> {
245        self.get_outpoints_mut()?
246            .retain(|_, scan_target| scan_target.block_height() <= truncate_height);
247
248        Ok(())
249    }
250}
251
252/// Trait for interfacing shard tree data with wallet data
253pub trait SyncShardTrees: SyncWallet {
254    /// Get reference to shard trees
255    fn get_shard_trees(&self) -> Result<&ShardTrees, Self::Error>;
256
257    /// Get mutable reference to shard trees
258    fn get_shard_trees_mut(&mut self) -> Result<&mut ShardTrees, Self::Error>;
259
260    /// Update wallet shard trees with new shard tree data.
261    ///
262    /// `highest_scanned_height` is the height of the highest scanned block in the wallet not including the `scan_range` we are updating.
263    #[allow(clippy::too_many_arguments)]
264    fn update_shard_trees(
265        &mut self,
266        consensus_parameters: &(impl consensus::Parameters + Sync),
267        fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
268        scan_range: &ScanRange,
269        highest_scanned_height: BlockHeight,
270        anchor_retention: Option<AnchorRetention>,
271        sapling_located_trees: Vec<LocatedTreeData<sapling_crypto::Node>>,
272        orchard_located_trees: Vec<LocatedTreeData<MerkleHashOrchard>>,
273        ironwood_located_trees: Vec<LocatedTreeData<MerkleHashOrchard>>,
274    ) -> impl std::future::Future<Output = Result<(), SyncError<Self::Error>>> + Send
275    where
276        Self: std::marker::Send,
277    {
278        async move {
279            let shard_trees = self.get_shard_trees_mut().map_err(SyncError::WalletError)?;
280
281            // limit the range that checkpoints are manually added to the top MAX_SHARDTREE_CHECKPOINTS scanned blocks for efficiency.
282            // As we sync the chain tip first and have spend-before-sync, we will always choose anchors very close to chain
283            // height and we will also never need to truncate to checkpoints lower than this height.
284            let checkpoint_range = if scan_range.block_range().start > highest_scanned_height {
285                let verification_window_start = scan_range
286                    .block_range()
287                    .end
288                    .saturating_sub(MAX_SHARDTREE_CHECKPOINTS);
289
290                std::cmp::max(scan_range.block_range().start, verification_window_start)
291                    ..scan_range.block_range().end
292            } else if scan_range.block_range().end
293                > highest_scanned_height.saturating_sub(MAX_SHARDTREE_CHECKPOINTS) + 1
294            {
295                let verification_window_start =
296                    highest_scanned_height.saturating_sub(MAX_SHARDTREE_CHECKPOINTS) + 1;
297
298                std::cmp::max(scan_range.block_range().start, verification_window_start)
299                    ..scan_range.block_range().end
300            } else {
301                BlockHeight::from_u32(0)..BlockHeight::from_u32(0)
302            };
303
304            // in the case that sapling and/or orchard and/or ironwood note commitments are not in an entire block there will be no retention
305            // at that height. Therefore, to prevent anchor and truncate errors, checkpoints are manually added first and
306            // copy the tree state from the previous checkpoint where the commitment tree has not changed as of that block.
307            let anchor_retention_window = anchor_retention.as_ref().map(|retention| {
308                let as_of = std::cmp::max(highest_scanned_height, scan_range.block_range().end - 1);
309                (
310                    retention,
311                    witness::anchor_retention_window(retention, as_of),
312                )
313            });
314            let checkpoint_heights = checkpoint_heights(
315                scan_range.block_range(),
316                &checkpoint_range,
317                anchor_retention_window
318                    .as_ref()
319                    .map(|(retention, window)| (*retention, window)),
320            );
321
322            // every checkpoint is determined before the shard trees are updated, as a checkpoint may be fetched from
323            // the server. a failed request then leaves the shard trees as they are.
324            let mut sapling_checkpoints = BTreeMap::new();
325            let mut orchard_checkpoints = BTreeMap::new();
326            let mut ironwood_checkpoints = BTreeMap::new();
327            for checkpoint_height in checkpoint_heights {
328                let sapling_checkpoint = determine_checkpoint::<
329                    Sapling,
330                    sapling_crypto::Node,
331                    { sapling_crypto::NOTE_COMMITMENT_TREE_DEPTH },
332                    { witness::SHARD_HEIGHT },
333                >(
334                    consensus_parameters,
335                    fetch_request_sender.clone(),
336                    checkpoint_height,
337                    &sapling_located_trees,
338                    &shard_trees.sapling,
339                    &sapling_checkpoints,
340                )
341                .await?;
342                sapling_checkpoints.insert(checkpoint_height, sapling_checkpoint);
343                let orchard_checkpoint = determine_checkpoint::<
344                    Orchard,
345                    MerkleHashOrchard,
346                    { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
347                    { witness::SHARD_HEIGHT },
348                >(
349                    consensus_parameters,
350                    fetch_request_sender.clone(),
351                    checkpoint_height,
352                    &orchard_located_trees,
353                    &shard_trees.orchard,
354                    &orchard_checkpoints,
355                )
356                .await?;
357                orchard_checkpoints.insert(checkpoint_height, orchard_checkpoint);
358                let ironwood_checkpoint = determine_checkpoint::<
359                    Ironwood,
360                    MerkleHashOrchard,
361                    { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
362                    { witness::SHARD_HEIGHT },
363                >(
364                    consensus_parameters,
365                    fetch_request_sender.clone(),
366                    checkpoint_height,
367                    &ironwood_located_trees,
368                    &shard_trees.ironwood,
369                    &ironwood_checkpoints,
370                )
371                .await?;
372                ironwood_checkpoints.insert(checkpoint_height, ironwood_checkpoint);
373            }
374
375            if let Some((retention, window)) = &anchor_retention_window {
376                witness::repin_anchor_checkpoints(
377                    retention,
378                    window,
379                    shard_trees.sapling.store_mut(),
380                );
381                witness::repin_anchor_checkpoints(
382                    retention,
383                    window,
384                    shard_trees.orchard.store_mut(),
385                );
386                witness::repin_anchor_checkpoints(
387                    retention,
388                    window,
389                    shard_trees.ironwood.store_mut(),
390                );
391            }
392            for (checkpoint_height, checkpoint) in sapling_checkpoints {
393                shard_trees
394                    .sapling
395                    .store_mut()
396                    .add_checkpoint(checkpoint_height, checkpoint)
397                    .expect("infallible");
398            }
399            for (checkpoint_height, checkpoint) in orchard_checkpoints {
400                shard_trees
401                    .orchard
402                    .store_mut()
403                    .add_checkpoint(checkpoint_height, checkpoint)
404                    .expect("infallible");
405            }
406            for (checkpoint_height, checkpoint) in ironwood_checkpoints {
407                shard_trees
408                    .ironwood
409                    .store_mut()
410                    .add_checkpoint(checkpoint_height, checkpoint)
411                    .expect("infallible");
412            }
413
414            // TODO: use `batch_insert_trees`
415            for tree in sapling_located_trees {
416                shard_trees
417                    .sapling
418                    .insert_tree(tree.subtree, tree.checkpoints)?;
419            }
420            for tree in orchard_located_trees {
421                shard_trees
422                    .orchard
423                    .insert_tree(tree.subtree, tree.checkpoints)?;
424            }
425            for tree in ironwood_located_trees {
426                shard_trees
427                    .ironwood
428                    .insert_tree(tree.subtree, tree.checkpoints)?;
429            }
430
431            Ok(())
432        }
433    }
434
435    /// Replaces all three shard trees with empty trees.
436    fn clear_shard_trees(&mut self) -> Result<(), SyncError<Self::Error>> {
437        let shard_trees = self.get_shard_trees_mut().map_err(SyncError::WalletError)?;
438        tracing::info!("Clearing shard trees.");
439        shard_trees.sapling = empty_shard_tree();
440        shard_trees.orchard = empty_shard_tree();
441        shard_trees.ironwood = empty_shard_tree();
442
443        Ok(())
444    }
445
446    /// Removes all shard tree data above the given `truncate_height`:
447    /// each tree rolls back to its checkpoint at that height, stays
448    /// untouched because it records nothing above it, or, holding state
449    /// it cannot roll back, aborts with
450    /// [`SyncError::TruncationError`] so the caller can fall back to the
451    /// clear-and-rescan recovery. Each tree's outcome is decided by the
452    /// pure per-pool rule `plan_pool_truncation` over facts read here
453    /// at the point of application (see [`crate::sync::truncate`]).
454    fn truncate_shard_trees(
455        &mut self,
456        truncate_height: BlockHeight,
457    ) -> Result<(), SyncError<Self::Error>> {
458        let shard_trees = self.get_shard_trees_mut().map_err(SyncError::WalletError)?;
459        truncate_pool_tree(&mut shard_trees.sapling, truncate_height, PoolType::SAPLING)?;
460        truncate_pool_tree(&mut shard_trees.orchard, truncate_height, PoolType::ORCHARD)?;
461        truncate_pool_tree(
462            &mut shard_trees.ironwood,
463            truncate_height,
464            PoolType::IRONWOOD,
465        )?;
466
467        Ok(())
468    }
469}
470
471/// Truncates one pool's shard tree: reads the tree's facts, decides its
472/// outcome through the pure per-pool rule ([`plan_pool_truncation`]),
473/// and applies it.
474///
475/// [`PoolTruncation::ToCheckpoint`] rolls the tree back to its
476/// checkpoint at `truncate_height`. [`PoolTruncation::Untouched`] leaves
477/// the tree alone. [`PoolTruncation::RequiresRescan`], and a planned
478/// rollback the tree store unexpectedly refuses, becomes
479/// [`SyncError::TruncationError`] naming the pool, so the caller can
480/// fall back to the clear-and-rescan recovery.
481/// Returns every block of `checkpoint_range` followed by the anchor retention boundaries that lie inside both
482/// `block_range` and the retention window and outside `checkpoint_range`.
483fn checkpoint_heights(
484    block_range: &Range<BlockHeight>,
485    checkpoint_range: &Range<BlockHeight>,
486    anchor_retention_window: Option<(&AnchorRetention, &RangeInclusive<BlockHeight>)>,
487) -> Vec<BlockHeight> {
488    let mut heights = (u32::from(checkpoint_range.start)..u32::from(checkpoint_range.end))
489        .map(BlockHeight::from_u32)
490        .collect::<Vec<_>>();
491    if let Some((retention, window)) = anchor_retention_window {
492        let start = std::cmp::max(*window.start(), block_range.start);
493        let end = std::cmp::min(*window.end(), block_range.end - 1);
494        heights.extend(
495            retention
496                .retained_in_range(start..=end)
497                .into_iter()
498                .filter(|boundary| !checkpoint_range.contains(boundary)),
499        );
500    }
501
502    heights
503}
504
505fn truncate_pool_tree<H, E, const DEPTH: u8, const SHARD_HEIGHT: u8>(
506    tree: &mut ShardTree<MemoryShardStore<H, BlockHeight>, DEPTH, SHARD_HEIGHT>,
507    truncate_height: BlockHeight,
508    pool: PoolType,
509) -> Result<(), SyncError<E>>
510where
511    H: Hashable + Clone + PartialEq,
512    E: std::fmt::Debug + std::fmt::Display,
513{
514    match plan_pool_truncation(tree_facts(tree, truncate_height), truncate_height) {
515        PoolTruncation::Untouched => Ok(()),
516        PoolTruncation::ToCheckpoint { checkpoint } => {
517            match tree.rollback_to_checkpoint(checkpoint)? {
518                RollbackOutcome::RolledBack => Ok(()),
519                RollbackOutcome::NoSuchCheckpoint => {
520                    tracing::error!(
521                        "{pool} shard tree refused the planned rollback to its checkpoint at \
522                         {checkpoint}! Beginning rescan."
523                    );
524                    Err(SyncError::TruncationError(truncate_height, pool))
525                }
526            }
527        }
528        PoolTruncation::RequiresRescan { newest_checkpoint } => {
529            tracing::error!(
530                "{pool} shard tree holds state up to checkpoint {newest_checkpoint}, above the \
531                 truncation target {truncate_height}, with no checkpoint at the target! \
532                 Beginning rescan."
533            );
534            Err(SyncError::TruncationError(truncate_height, pool))
535        }
536    }
537}
538
539/// Determines the checkpoint of `shard_tree` at `checkpoint_height`. The shard tree is only read.
540///
541/// The checkpoint is taken from the `located_trees` where the block holds note commitments of the pool. Otherwise
542/// the pool's commitment tree is unchanged as of the block, so the tree state is copied from the checkpoint of the
543/// block below, looked up in `determined_checkpoints` and then in the shard tree, or fetched from the server where
544/// neither holds it.
545// TODO: move into `update_shard_trees` trait method
546async fn determine_checkpoint<D, L, const DEPTH: u8, const SHARD_HEIGHT: u8>(
547    consensus_parameters: &impl consensus::Parameters,
548    fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
549    checkpoint_height: BlockHeight,
550    located_trees: &[LocatedTreeData<L>],
551    shard_tree: &shardtree::ShardTree<
552        shardtree::store::memory::MemoryShardStore<L, BlockHeight>,
553        DEPTH,
554        SHARD_HEIGHT,
555    >,
556    determined_checkpoints: &BTreeMap<BlockHeight, Checkpoint>,
557) -> Result<Checkpoint, ServerError>
558where
559    L: Clone + PartialEq + incrementalmerkletree::Hashable,
560    D: SyncDomain,
561{
562    if let Some((_, position)) = located_trees
563        .iter()
564        .flat_map(|tree| tree.checkpoints.iter())
565        .find(|(height, _)| **height == checkpoint_height)
566    {
567        return Ok(Checkpoint::at_position(*position));
568    }
569
570    let mut previous_checkpoint = determined_checkpoints
571        .get(&(checkpoint_height - 1))
572        .cloned();
573    if previous_checkpoint.is_none() {
574        shard_tree
575            .store()
576            .for_each_checkpoint(
577                MAX_SHARDTREE_CHECKPOINTS as usize + 100,
578                |height, checkpoint| {
579                    if *height == checkpoint_height - 1 {
580                        previous_checkpoint = Some(checkpoint.clone());
581                    }
582                    Ok(())
583                },
584            )
585            .expect("infallible");
586    }
587
588    let tree_state = if let Some(checkpoint) = previous_checkpoint {
589        checkpoint.tree_state()
590    } else {
591        let frontiers = client::get_frontiers(
592            fetch_request_sender.clone(),
593            consensus_parameters,
594            checkpoint_height,
595        )
596        .await?;
597        let tree_size = match D::SHIELDED_PROTOCOL {
598            ShieldedPool::Sapling => frontiers.final_sapling_tree().tree_size(),
599            ShieldedPool::Orchard => frontiers.final_orchard_tree().tree_size(),
600            ShieldedPool::Ironwood => frontiers.final_ironwood_tree().tree_size(),
601        };
602        if tree_size == 0 {
603            TreeState::Empty
604        } else {
605            TreeState::AtPosition(incrementalmerkletree::Position::from(tree_size - 1))
606        }
607    };
608
609    Ok(Checkpoint::from_parts(tree_state, BTreeSet::new()))
610}