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