Skip to main content

zebra_state/service/finalized_state/zebra_db/
shielded.rs

1//! Provides high-level access to database shielded:
2//! - nullifiers
3//! - note commitment trees
4//! - anchors
5//!
6//! This module makes sure that:
7//! - all disk writes happen inside a RocksDB transaction, and
8//! - format-specific invariants are maintained.
9//!
10//! # Correctness
11//!
12//! [`crate::constants::state_database_format_version_in_code()`] must be incremented
13//! each time the database format (column, serialization, etc) changes.
14
15use std::{
16    collections::{BTreeMap, HashMap},
17    sync::Arc,
18};
19
20use zebra_chain::{
21    block::Height,
22    orchard,
23    parallel::tree::NoteCommitmentTrees,
24    sapling, sprout,
25    subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex},
26    transaction::Transaction,
27};
28
29use crate::{
30    request::{FinalizedBlock, Treestate},
31    service::finalized_state::{
32        disk_db::{DiskWriteBatch, ReadDisk, WriteDisk},
33        disk_format::RawBytes,
34        zebra_db::ZebraDb,
35    },
36    TransactionLocation,
37};
38
39// Doc-only items
40#[allow(unused_imports)]
41use zebra_chain::subtree::NoteCommitmentSubtree;
42
43impl ZebraDb {
44    // Read shielded methods
45
46    /// Returns `true` if the finalized state contains `sprout_nullifier`.
47    pub fn contains_sprout_nullifier(&self, sprout_nullifier: &sprout::Nullifier) -> bool {
48        let sprout_nullifiers = self.db.cf_handle("sprout_nullifiers").unwrap();
49        self.db.zs_contains(&sprout_nullifiers, &sprout_nullifier)
50    }
51
52    /// Returns `true` if the finalized state contains `sapling_nullifier`.
53    pub fn contains_sapling_nullifier(&self, sapling_nullifier: &sapling::Nullifier) -> bool {
54        let sapling_nullifiers = self.db.cf_handle("sapling_nullifiers").unwrap();
55        self.db.zs_contains(&sapling_nullifiers, &sapling_nullifier)
56    }
57
58    /// Returns `true` if the finalized state contains `orchard_nullifier`.
59    pub fn contains_orchard_nullifier(&self, orchard_nullifier: &orchard::Nullifier) -> bool {
60        let orchard_nullifiers = self.db.cf_handle("orchard_nullifiers").unwrap();
61        self.db.zs_contains(&orchard_nullifiers, &orchard_nullifier)
62    }
63
64    /// Returns the [`TransactionLocation`] of the transaction that revealed
65    /// the given [`sprout::Nullifier`], if it is revealed in the finalized state and its
66    /// spending transaction hash has been indexed.
67    #[allow(clippy::unwrap_in_result)]
68    pub fn sprout_revealing_tx_loc(
69        &self,
70        sprout_nullifier: &sprout::Nullifier,
71    ) -> Option<TransactionLocation> {
72        let sprout_nullifiers = self.db.cf_handle("sprout_nullifiers").unwrap();
73        self.db.zs_get(&sprout_nullifiers, &sprout_nullifier)?
74    }
75
76    /// Returns the [`TransactionLocation`] of the transaction that revealed
77    /// the given [`sapling::Nullifier`], if it is revealed in the finalized state and its
78    /// spending transaction hash has been indexed.
79    #[allow(clippy::unwrap_in_result)]
80    pub fn sapling_revealing_tx_loc(
81        &self,
82        sapling_nullifier: &sapling::Nullifier,
83    ) -> Option<TransactionLocation> {
84        let sapling_nullifiers = self.db.cf_handle("sapling_nullifiers").unwrap();
85        self.db.zs_get(&sapling_nullifiers, &sapling_nullifier)?
86    }
87
88    /// Returns the [`TransactionLocation`] of the transaction that revealed
89    /// the given [`orchard::Nullifier`], if it is revealed in the finalized state and its
90    /// spending transaction hash has been indexed.
91    #[allow(clippy::unwrap_in_result)]
92    pub fn orchard_revealing_tx_loc(
93        &self,
94        orchard_nullifier: &orchard::Nullifier,
95    ) -> Option<TransactionLocation> {
96        let orchard_nullifiers = self.db.cf_handle("orchard_nullifiers").unwrap();
97        self.db.zs_get(&orchard_nullifiers, &orchard_nullifier)?
98    }
99
100    /// Returns `true` if the finalized state contains `sprout_anchor`.
101    #[allow(dead_code)]
102    pub fn contains_sprout_anchor(&self, sprout_anchor: &sprout::tree::Root) -> bool {
103        let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap();
104        self.db.zs_contains(&sprout_anchors, &sprout_anchor)
105    }
106
107    /// Returns `true` if the finalized state contains `sapling_anchor`.
108    pub fn contains_sapling_anchor(&self, sapling_anchor: &sapling::tree::Root) -> bool {
109        let sapling_anchors = self.db.cf_handle("sapling_anchors").unwrap();
110        self.db.zs_contains(&sapling_anchors, &sapling_anchor)
111    }
112
113    /// Returns `true` if the finalized state contains `orchard_anchor`.
114    pub fn contains_orchard_anchor(&self, orchard_anchor: &orchard::tree::Root) -> bool {
115        let orchard_anchors = self.db.cf_handle("orchard_anchors").unwrap();
116        self.db.zs_contains(&orchard_anchors, &orchard_anchor)
117    }
118
119    // # Sprout trees
120
121    /// Returns the Sprout note commitment tree of the finalized tip
122    /// or the empty tree if the state is empty.
123    pub fn sprout_tree_for_tip(&self) -> Arc<sprout::tree::NoteCommitmentTree> {
124        if self.is_empty() {
125            return Arc::<sprout::tree::NoteCommitmentTree>::default();
126        }
127
128        let sprout_tree_cf = self.db.cf_handle("sprout_note_commitment_tree").unwrap();
129
130        // # Backwards Compatibility
131        //
132        // This code can read the column family format in 1.2.0 and earlier (tip height key),
133        // and after PR #7392 is merged (empty key). The height-based code can be removed when
134        // versions 1.2.0 and earlier are no longer supported.
135        //
136        // # Concurrency
137        //
138        // There is only one entry in this column family, which is atomically updated by a block
139        // write batch (database transaction). If we used a height as the column family tree,
140        // any updates between reading the tip height and reading the tree could cause panics.
141        //
142        // So we use the empty key `()`. Since the key has a constant value, we will always read
143        // the latest tree.
144        let mut sprout_tree: Option<Arc<sprout::tree::NoteCommitmentTree>> =
145            self.db.zs_get(&sprout_tree_cf, &());
146
147        if sprout_tree.is_none() {
148            // In Zebra 1.4.0 and later, we don't update the sprout tip tree unless it is changed.
149            // And we write with a `()` key, not a height key.
150            // So we need to look for the most recent update height if the `()` key has never been written.
151            sprout_tree = self
152                .db
153                .zs_last_key_value(&sprout_tree_cf)
154                .map(|(_key, tree_value): (Height, _)| tree_value);
155        }
156
157        sprout_tree.expect("Sprout note commitment tree must exist if there is a finalized tip")
158    }
159
160    /// Returns the Sprout note commitment tree matching the given anchor.
161    ///
162    /// This is used for interstitial tree building, which is unique to Sprout.
163    #[allow(clippy::unwrap_in_result)]
164    pub fn sprout_tree_by_anchor(
165        &self,
166        sprout_anchor: &sprout::tree::Root,
167    ) -> Option<Arc<sprout::tree::NoteCommitmentTree>> {
168        let sprout_anchors_handle = self.db.cf_handle("sprout_anchors").unwrap();
169
170        self.db
171            .zs_get(&sprout_anchors_handle, sprout_anchor)
172            .map(Arc::new)
173    }
174
175    /// Returns all the Sprout note commitment trees in the database.
176    ///
177    /// Calling this method can load a lot of data into RAM, and delay block commit transactions.
178    #[allow(dead_code)]
179    pub fn sprout_trees_full_map(
180        &self,
181    ) -> HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>> {
182        let sprout_anchors_handle = self.db.cf_handle("sprout_anchors").unwrap();
183
184        self.db
185            .zs_items_in_range_unordered(&sprout_anchors_handle, ..)
186    }
187
188    /// Returns all the Sprout note commitment tip trees.
189    /// We only store the sprout tree for the tip, so this method is mainly used in tests.
190    pub fn sprout_trees_full_tip(
191        &self,
192    ) -> impl Iterator<Item = (RawBytes, Arc<sprout::tree::NoteCommitmentTree>)> + '_ {
193        let sprout_trees = self.db.cf_handle("sprout_note_commitment_tree").unwrap();
194        self.db.zs_forward_range_iter(&sprout_trees, ..)
195    }
196
197    // # Sapling trees
198
199    /// Returns the Sapling note commitment tree of the finalized tip or the empty tree if the state
200    /// is empty.
201    pub fn sapling_tree_for_tip(&self) -> Arc<sapling::tree::NoteCommitmentTree> {
202        let height = match self.finalized_tip_height() {
203            Some(h) => h,
204            None => return Default::default(),
205        };
206
207        self.sapling_tree_by_height(&height)
208            .expect("Sapling note commitment tree must exist if there is a finalized tip")
209    }
210
211    /// Returns the Sapling note commitment tree matching the given block height, or `None` if the
212    /// height is above the finalized tip.
213    #[allow(clippy::unwrap_in_result)]
214    pub fn sapling_tree_by_height(
215        &self,
216        height: &Height,
217    ) -> Option<Arc<sapling::tree::NoteCommitmentTree>> {
218        let tip_height = self.finalized_tip_height()?;
219
220        // If we're above the tip, searching backwards would always return the tip tree.
221        // But the correct answer is "we don't know that tree yet".
222        if *height > tip_height {
223            return None;
224        }
225
226        let sapling_trees = self.db.cf_handle("sapling_note_commitment_tree").unwrap();
227
228        // If we know there must be a tree, search backwards for it.
229        let (_first_duplicate_height, tree) = self
230            .db
231            .zs_prev_key_value_back_from(&sapling_trees, height)
232            .expect(
233                "Sapling note commitment trees must exist for all heights below the finalized tip",
234            );
235
236        Some(Arc::new(tree))
237    }
238
239    /// Returns the Sapling note commitment trees in the supplied range, in increasing height order.
240    pub fn sapling_tree_by_height_range<R>(
241        &self,
242        range: R,
243    ) -> impl Iterator<Item = (Height, Arc<sapling::tree::NoteCommitmentTree>)> + '_
244    where
245        R: std::ops::RangeBounds<Height>,
246    {
247        let sapling_trees = self.db.cf_handle("sapling_note_commitment_tree").unwrap();
248        self.db.zs_forward_range_iter(&sapling_trees, range)
249    }
250
251    /// Returns the Sapling note commitment trees in the reversed range, in decreasing height order.
252    pub fn sapling_tree_by_reversed_height_range<R>(
253        &self,
254        range: R,
255    ) -> impl Iterator<Item = (Height, Arc<sapling::tree::NoteCommitmentTree>)> + '_
256    where
257        R: std::ops::RangeBounds<Height>,
258    {
259        let sapling_trees = self.db.cf_handle("sapling_note_commitment_tree").unwrap();
260        self.db.zs_reverse_range_iter(&sapling_trees, range)
261    }
262
263    /// Returns the Sapling note commitment subtree at this `index`.
264    ///
265    /// # Correctness
266    ///
267    /// This method should not be used to get subtrees for RPC responses,
268    /// because those subtree lists require that the start subtree is present in the list.
269    /// Instead, use `sapling_subtree_list_by_index_for_rpc()`.
270    #[allow(clippy::unwrap_in_result)]
271    pub(in super::super) fn sapling_subtree_by_index(
272        &self,
273        index: impl Into<NoteCommitmentSubtreeIndex> + Copy,
274    ) -> Option<NoteCommitmentSubtree<sapling_crypto::Node>> {
275        let sapling_subtrees = self
276            .db
277            .cf_handle("sapling_note_commitment_subtree")
278            .unwrap();
279
280        let subtree_data: NoteCommitmentSubtreeData<sapling_crypto::Node> =
281            self.db.zs_get(&sapling_subtrees, &index.into())?;
282
283        Some(subtree_data.with_index(index))
284    }
285
286    /// Returns a list of Sapling [`NoteCommitmentSubtree`]s in the provided range.
287    #[allow(clippy::unwrap_in_result)]
288    pub fn sapling_subtree_list_by_index_range(
289        &self,
290        range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex>,
291    ) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<sapling_crypto::Node>> {
292        let sapling_subtrees = self
293            .db
294            .cf_handle("sapling_note_commitment_subtree")
295            .unwrap();
296
297        self.db
298            .zs_forward_range_iter(&sapling_subtrees, range)
299            .collect()
300    }
301
302    /// Get the sapling note commitment subtress for the finalized tip.
303    #[allow(clippy::unwrap_in_result)]
304    fn sapling_subtree_for_tip(&self) -> Option<NoteCommitmentSubtree<sapling_crypto::Node>> {
305        let sapling_subtrees = self
306            .db
307            .cf_handle("sapling_note_commitment_subtree")
308            .unwrap();
309
310        let (index, subtree_data): (
311            NoteCommitmentSubtreeIndex,
312            NoteCommitmentSubtreeData<sapling_crypto::Node>,
313        ) = self.db.zs_last_key_value(&sapling_subtrees)?;
314
315        let tip_height = self.finalized_tip_height()?;
316        if subtree_data.end_height != tip_height {
317            return None;
318        }
319
320        Some(subtree_data.with_index(index))
321    }
322
323    // Orchard trees
324
325    /// Returns the Orchard note commitment tree of the finalized tip or the empty tree if the state
326    /// is empty.
327    pub fn orchard_tree_for_tip(&self) -> Arc<orchard::tree::NoteCommitmentTree> {
328        let height = match self.finalized_tip_height() {
329            Some(h) => h,
330            None => return Default::default(),
331        };
332
333        self.orchard_tree_by_height(&height)
334            .expect("Orchard note commitment tree must exist if there is a finalized tip")
335    }
336
337    /// Returns the Orchard note commitment tree matching the given block height,
338    /// or `None` if the height is above the finalized tip.
339    #[allow(clippy::unwrap_in_result)]
340    pub fn orchard_tree_by_height(
341        &self,
342        height: &Height,
343    ) -> Option<Arc<orchard::tree::NoteCommitmentTree>> {
344        let tip_height = self.finalized_tip_height()?;
345
346        // If we're above the tip, searching backwards would always return the tip tree.
347        // But the correct answer is "we don't know that tree yet".
348        if *height > tip_height {
349            return None;
350        }
351
352        let orchard_trees = self.db.cf_handle("orchard_note_commitment_tree").unwrap();
353
354        // If we know there must be a tree, search backwards for it.
355        let (_first_duplicate_height, tree) = self
356            .db
357            .zs_prev_key_value_back_from(&orchard_trees, height)
358            .expect(
359                "Orchard note commitment trees must exist for all heights below the finalized tip",
360            );
361
362        Some(Arc::new(tree))
363    }
364
365    /// Returns the Orchard note commitment trees in the supplied range, in increasing height order.
366    pub fn orchard_tree_by_height_range<R>(
367        &self,
368        range: R,
369    ) -> impl Iterator<Item = (Height, Arc<orchard::tree::NoteCommitmentTree>)> + '_
370    where
371        R: std::ops::RangeBounds<Height>,
372    {
373        let orchard_trees = self.db.cf_handle("orchard_note_commitment_tree").unwrap();
374        self.db.zs_forward_range_iter(&orchard_trees, range)
375    }
376
377    /// Returns the Orchard note commitment trees in the reversed range, in decreasing height order.
378    pub fn orchard_tree_by_reversed_height_range<R>(
379        &self,
380        range: R,
381    ) -> impl Iterator<Item = (Height, Arc<orchard::tree::NoteCommitmentTree>)> + '_
382    where
383        R: std::ops::RangeBounds<Height>,
384    {
385        let orchard_trees = self.db.cf_handle("orchard_note_commitment_tree").unwrap();
386        self.db.zs_reverse_range_iter(&orchard_trees, range)
387    }
388
389    /// Returns the Orchard note commitment subtree at this `index`.
390    ///
391    /// # Correctness
392    ///
393    /// This method should not be used to get subtrees for RPC responses,
394    /// because those subtree lists require that the start subtree is present in the list.
395    /// Instead, use `orchard_subtree_list_by_index_for_rpc()`.
396    #[allow(clippy::unwrap_in_result)]
397    pub(in super::super) fn orchard_subtree_by_index(
398        &self,
399        index: impl Into<NoteCommitmentSubtreeIndex> + Copy,
400    ) -> Option<NoteCommitmentSubtree<orchard::tree::Node>> {
401        let orchard_subtrees = self
402            .db
403            .cf_handle("orchard_note_commitment_subtree")
404            .unwrap();
405
406        let subtree_data: NoteCommitmentSubtreeData<orchard::tree::Node> =
407            self.db.zs_get(&orchard_subtrees, &index.into())?;
408
409        Some(subtree_data.with_index(index))
410    }
411
412    /// Returns a list of Orchard [`NoteCommitmentSubtree`]s in the provided range.
413    #[allow(clippy::unwrap_in_result)]
414    pub fn orchard_subtree_list_by_index_range(
415        &self,
416        range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex>,
417    ) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>> {
418        let orchard_subtrees = self
419            .db
420            .cf_handle("orchard_note_commitment_subtree")
421            .unwrap();
422
423        self.db
424            .zs_forward_range_iter(&orchard_subtrees, range)
425            .collect()
426    }
427
428    /// Get the orchard note commitment subtress for the finalized tip.
429    #[allow(clippy::unwrap_in_result)]
430    fn orchard_subtree_for_tip(&self) -> Option<NoteCommitmentSubtree<orchard::tree::Node>> {
431        let orchard_subtrees = self
432            .db
433            .cf_handle("orchard_note_commitment_subtree")
434            .unwrap();
435
436        let (index, subtree_data): (
437            NoteCommitmentSubtreeIndex,
438            NoteCommitmentSubtreeData<orchard::tree::Node>,
439        ) = self.db.zs_last_key_value(&orchard_subtrees)?;
440
441        let tip_height = self.finalized_tip_height()?;
442        if subtree_data.end_height != tip_height {
443            return None;
444        }
445
446        Some(subtree_data.with_index(index))
447    }
448
449    /// Returns the shielded note commitment trees of the finalized tip
450    /// or the empty trees if the state is empty.
451    /// Additionally, returns the sapling and orchard subtrees for the finalized tip if
452    /// the current subtree is finalizing in the tip, None otherwise.
453    pub fn note_commitment_trees_for_tip(&self) -> NoteCommitmentTrees {
454        NoteCommitmentTrees {
455            sprout: self.sprout_tree_for_tip(),
456            sapling: self.sapling_tree_for_tip(),
457            sapling_subtree: self.sapling_subtree_for_tip(),
458            orchard: self.orchard_tree_for_tip(),
459            orchard_subtree: self.orchard_subtree_for_tip(),
460        }
461    }
462}
463
464impl DiskWriteBatch {
465    /// Prepare a database batch containing `finalized.block`'s shielded transaction indexes,
466    /// and return it (without actually writing anything).
467    ///
468    /// If this method returns an error, it will be propagated,
469    /// and the batch should not be written to the database.
470    pub fn prepare_shielded_transaction_batch(
471        &mut self,
472        zebra_db: &ZebraDb,
473        finalized: &FinalizedBlock,
474    ) {
475        #[cfg(feature = "indexer")]
476        let FinalizedBlock { block, height, .. } = finalized;
477
478        // Index each transaction's shielded data
479        #[cfg(feature = "indexer")]
480        for (tx_index, transaction) in block.transactions.iter().enumerate() {
481            let tx_loc = TransactionLocation::from_usize(*height, tx_index);
482            self.prepare_nullifier_batch(zebra_db, transaction, tx_loc);
483        }
484
485        #[cfg(not(feature = "indexer"))]
486        for transaction in &finalized.block.transactions {
487            self.prepare_nullifier_batch(zebra_db, transaction);
488        }
489    }
490
491    /// Prepare a database batch containing `finalized.block`'s nullifiers,
492    /// and return it (without actually writing anything).
493    ///
494    /// # Errors
495    ///
496    /// - This method doesn't currently return any errors, but it might in future
497    #[allow(clippy::unwrap_in_result)]
498    pub fn prepare_nullifier_batch(
499        &mut self,
500        zebra_db: &ZebraDb,
501        transaction: &Transaction,
502        #[cfg(feature = "indexer")] transaction_location: TransactionLocation,
503    ) {
504        let db = &zebra_db.db;
505        let sprout_nullifiers = db.cf_handle("sprout_nullifiers").unwrap();
506        let sapling_nullifiers = db.cf_handle("sapling_nullifiers").unwrap();
507        let orchard_nullifiers = db.cf_handle("orchard_nullifiers").unwrap();
508
509        #[cfg(feature = "indexer")]
510        let insert_value = transaction_location;
511        #[cfg(not(feature = "indexer"))]
512        let insert_value = ();
513
514        // Mark sprout, sapling and orchard nullifiers as spent
515        for sprout_nullifier in transaction.sprout_nullifiers() {
516            self.zs_insert(&sprout_nullifiers, sprout_nullifier, insert_value);
517        }
518        for sapling_nullifier in transaction.sapling_nullifiers() {
519            self.zs_insert(&sapling_nullifiers, sapling_nullifier, insert_value);
520        }
521        for orchard_nullifier in transaction.orchard_nullifiers() {
522            self.zs_insert(&orchard_nullifiers, orchard_nullifier, insert_value);
523        }
524    }
525
526    /// Prepare a database batch containing the note commitment and history tree updates
527    /// from `finalized.block`, and return it (without actually writing anything).
528    ///
529    /// If this method returns an error, it will be propagated,
530    /// and the batch should not be written to the database.
531    #[allow(clippy::unwrap_in_result)]
532    pub fn prepare_trees_batch(
533        &mut self,
534        zebra_db: &ZebraDb,
535        finalized: &FinalizedBlock,
536        prev_note_commitment_trees: Option<NoteCommitmentTrees>,
537    ) {
538        let FinalizedBlock {
539            height,
540            treestate:
541                Treestate {
542                    note_commitment_trees,
543                    history_tree,
544                },
545            ..
546        } = finalized;
547
548        let prev_sprout_tree = prev_note_commitment_trees.as_ref().map_or_else(
549            || zebra_db.sprout_tree_for_tip(),
550            |prev_trees| prev_trees.sprout.clone(),
551        );
552        let prev_sapling_tree = prev_note_commitment_trees.as_ref().map_or_else(
553            || zebra_db.sapling_tree_for_tip(),
554            |prev_trees| prev_trees.sapling.clone(),
555        );
556        let prev_orchard_tree = prev_note_commitment_trees.as_ref().map_or_else(
557            || zebra_db.orchard_tree_for_tip(),
558            |prev_trees| prev_trees.orchard.clone(),
559        );
560
561        // Update the Sprout tree and store its anchor only if it has changed
562        if height.is_min() || prev_sprout_tree != note_commitment_trees.sprout {
563            self.update_sprout_tree(zebra_db, &note_commitment_trees.sprout)
564        }
565
566        // Store the Sapling tree, anchor, and any new subtrees only if they have changed
567        if height.is_min() || prev_sapling_tree != note_commitment_trees.sapling {
568            self.create_sapling_tree(zebra_db, height, &note_commitment_trees.sapling);
569
570            if let Some(subtree) = note_commitment_trees.sapling_subtree {
571                self.insert_sapling_subtree(zebra_db, &subtree);
572            }
573        }
574
575        // Store the Orchard tree, anchor, and any new subtrees only if they have changed
576        if height.is_min() || prev_orchard_tree != note_commitment_trees.orchard {
577            self.create_orchard_tree(zebra_db, height, &note_commitment_trees.orchard);
578
579            if let Some(subtree) = note_commitment_trees.orchard_subtree {
580                self.insert_orchard_subtree(zebra_db, &subtree);
581            }
582        }
583
584        self.update_history_tree(zebra_db, history_tree);
585    }
586
587    // Sprout tree methods
588
589    /// Updates the Sprout note commitment tree for the tip, and the Sprout anchors.
590    pub fn update_sprout_tree(
591        &mut self,
592        zebra_db: &ZebraDb,
593        tree: &sprout::tree::NoteCommitmentTree,
594    ) {
595        let sprout_anchors = zebra_db.db.cf_handle("sprout_anchors").unwrap();
596        let sprout_tree_cf = zebra_db
597            .db
598            .cf_handle("sprout_note_commitment_tree")
599            .unwrap();
600
601        // Sprout lookups need all previous trees by their anchors.
602        // The root must be calculated first, so it is cached in the database.
603        self.zs_insert(&sprout_anchors, tree.root(), tree);
604        self.zs_insert(&sprout_tree_cf, (), tree);
605    }
606
607    /// Legacy method: Deletes the range of Sprout note commitment trees at the given [`Height`]s.
608    /// Doesn't delete anchors from the anchor index. Doesn't delete the upper bound.
609    ///
610    /// From state format 25.3.0 onwards, the Sprout trees are indexed by an empty key,
611    /// so this method does nothing.
612    pub fn delete_range_sprout_tree(&mut self, zebra_db: &ZebraDb, from: &Height, to: &Height) {
613        let sprout_tree_cf = zebra_db
614            .db
615            .cf_handle("sprout_note_commitment_tree")
616            .unwrap();
617
618        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
619        self.zs_delete_range(&sprout_tree_cf, from, to);
620    }
621
622    /// Deletes the given Sprout note commitment tree `anchor`.
623    #[allow(dead_code)]
624    pub fn delete_sprout_anchor(&mut self, zebra_db: &ZebraDb, anchor: &sprout::tree::Root) {
625        let sprout_anchors = zebra_db.db.cf_handle("sprout_anchors").unwrap();
626        self.zs_delete(&sprout_anchors, anchor);
627    }
628
629    // Sapling tree methods
630
631    /// Inserts or overwrites the Sapling note commitment tree at the given [`Height`],
632    /// and the Sapling anchors.
633    pub fn create_sapling_tree(
634        &mut self,
635        zebra_db: &ZebraDb,
636        height: &Height,
637        tree: &sapling::tree::NoteCommitmentTree,
638    ) {
639        let sapling_anchors = zebra_db.db.cf_handle("sapling_anchors").unwrap();
640        let sapling_tree_cf = zebra_db
641            .db
642            .cf_handle("sapling_note_commitment_tree")
643            .unwrap();
644
645        self.zs_insert(&sapling_anchors, tree.root(), ());
646        self.zs_insert(&sapling_tree_cf, height, tree);
647    }
648
649    /// Inserts the Sapling note commitment subtree into the batch.
650    pub fn insert_sapling_subtree(
651        &mut self,
652        zebra_db: &ZebraDb,
653        subtree: &NoteCommitmentSubtree<sapling_crypto::Node>,
654    ) {
655        let sapling_subtree_cf = zebra_db
656            .db
657            .cf_handle("sapling_note_commitment_subtree")
658            .unwrap();
659        self.zs_insert(&sapling_subtree_cf, subtree.index, subtree.into_data());
660    }
661
662    /// Deletes the Sapling note commitment tree at the given [`Height`].
663    pub fn delete_sapling_tree(&mut self, zebra_db: &ZebraDb, height: &Height) {
664        let sapling_tree_cf = zebra_db
665            .db
666            .cf_handle("sapling_note_commitment_tree")
667            .unwrap();
668        self.zs_delete(&sapling_tree_cf, height);
669    }
670
671    /// Deletes the range of Sapling note commitment trees at the given [`Height`]s.
672    /// Doesn't delete anchors from the anchor index. Doesn't delete the upper bound.
673    #[allow(dead_code)]
674    pub fn delete_range_sapling_tree(&mut self, zebra_db: &ZebraDb, from: &Height, to: &Height) {
675        let sapling_tree_cf = zebra_db
676            .db
677            .cf_handle("sapling_note_commitment_tree")
678            .unwrap();
679
680        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
681        self.zs_delete_range(&sapling_tree_cf, from, to);
682    }
683
684    /// Deletes the given Sapling note commitment tree `anchor`.
685    #[allow(dead_code)]
686    pub fn delete_sapling_anchor(&mut self, zebra_db: &ZebraDb, anchor: &sapling::tree::Root) {
687        let sapling_anchors = zebra_db.db.cf_handle("sapling_anchors").unwrap();
688        self.zs_delete(&sapling_anchors, anchor);
689    }
690
691    /// Deletes the range of Sapling subtrees at the given [`NoteCommitmentSubtreeIndex`]es.
692    /// Doesn't delete the upper bound.
693    pub fn delete_range_sapling_subtree(
694        &mut self,
695        zebra_db: &ZebraDb,
696        from: NoteCommitmentSubtreeIndex,
697        to: NoteCommitmentSubtreeIndex,
698    ) {
699        let sapling_subtree_cf = zebra_db
700            .db
701            .cf_handle("sapling_note_commitment_subtree")
702            .unwrap();
703
704        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
705        self.zs_delete_range(&sapling_subtree_cf, from, to);
706    }
707
708    // Orchard tree methods
709
710    /// Inserts or overwrites the Orchard note commitment tree at the given [`Height`],
711    /// and the Orchard anchors.
712    pub fn create_orchard_tree(
713        &mut self,
714        zebra_db: &ZebraDb,
715        height: &Height,
716        tree: &orchard::tree::NoteCommitmentTree,
717    ) {
718        let orchard_anchors = zebra_db.db.cf_handle("orchard_anchors").unwrap();
719        let orchard_tree_cf = zebra_db
720            .db
721            .cf_handle("orchard_note_commitment_tree")
722            .unwrap();
723
724        self.zs_insert(&orchard_anchors, tree.root(), ());
725        self.zs_insert(&orchard_tree_cf, height, tree);
726    }
727
728    /// Inserts the Orchard note commitment subtree into the batch.
729    pub fn insert_orchard_subtree(
730        &mut self,
731        zebra_db: &ZebraDb,
732        subtree: &NoteCommitmentSubtree<orchard::tree::Node>,
733    ) {
734        let orchard_subtree_cf = zebra_db
735            .db
736            .cf_handle("orchard_note_commitment_subtree")
737            .unwrap();
738        self.zs_insert(&orchard_subtree_cf, subtree.index, subtree.into_data());
739    }
740
741    /// Deletes the Orchard note commitment tree at the given [`Height`].
742    pub fn delete_orchard_tree(&mut self, zebra_db: &ZebraDb, height: &Height) {
743        let orchard_tree_cf = zebra_db
744            .db
745            .cf_handle("orchard_note_commitment_tree")
746            .unwrap();
747        self.zs_delete(&orchard_tree_cf, height);
748    }
749
750    /// Deletes the range of Orchard note commitment trees at the given [`Height`]s.
751    /// Doesn't delete anchors from the anchor index. Doesn't delete the upper bound.
752    #[allow(dead_code)]
753    pub fn delete_range_orchard_tree(&mut self, zebra_db: &ZebraDb, from: &Height, to: &Height) {
754        let orchard_tree_cf = zebra_db
755            .db
756            .cf_handle("orchard_note_commitment_tree")
757            .unwrap();
758
759        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
760        self.zs_delete_range(&orchard_tree_cf, from, to);
761    }
762
763    /// Deletes the given Orchard note commitment tree `anchor`.
764    #[allow(dead_code)]
765    pub fn delete_orchard_anchor(&mut self, zebra_db: &ZebraDb, anchor: &orchard::tree::Root) {
766        let orchard_anchors = zebra_db.db.cf_handle("orchard_anchors").unwrap();
767        self.zs_delete(&orchard_anchors, anchor);
768    }
769
770    /// Deletes the range of Orchard subtrees at the given [`NoteCommitmentSubtreeIndex`]es.
771    /// Doesn't delete the upper bound.
772    pub fn delete_range_orchard_subtree(
773        &mut self,
774        zebra_db: &ZebraDb,
775        from: NoteCommitmentSubtreeIndex,
776        to: NoteCommitmentSubtreeIndex,
777    ) {
778        let orchard_subtree_cf = zebra_db
779            .db
780            .cf_handle("orchard_note_commitment_subtree")
781            .unwrap();
782
783        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
784        self.zs_delete_range(&orchard_subtree_cf, from, to);
785    }
786}