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    ironwood, orchard,
23    parallel::tree::NoteCommitmentTrees,
24    parameters::NetworkUpgrade,
25    sapling, sprout,
26    subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex},
27    transaction::Transaction,
28};
29
30use crate::{
31    request::{FinalizedBlock, Treestate},
32    service::finalized_state::{
33        disk_db::{DiskWriteBatch, ReadDisk, WriteDisk},
34        disk_format::RawBytes,
35        zebra_db::ZebraDb,
36    },
37    TransactionLocation,
38};
39
40// Doc-only items
41#[allow(unused_imports)]
42use zebra_chain::subtree::NoteCommitmentSubtree;
43
44impl ZebraDb {
45    // Read shielded methods
46
47    /// Returns `true` if the finalized state contains `sprout_nullifier`.
48    pub fn contains_sprout_nullifier(&self, sprout_nullifier: &sprout::Nullifier) -> bool {
49        let sprout_nullifiers = self.db.cf_handle("sprout_nullifiers").unwrap();
50        self.db.zs_contains(&sprout_nullifiers, &sprout_nullifier)
51    }
52
53    /// Returns `true` if the finalized state contains `sapling_nullifier`.
54    pub fn contains_sapling_nullifier(&self, sapling_nullifier: &sapling::Nullifier) -> bool {
55        let sapling_nullifiers = self.db.cf_handle("sapling_nullifiers").unwrap();
56        self.db.zs_contains(&sapling_nullifiers, &sapling_nullifier)
57    }
58
59    /// Returns `true` if the finalized state contains `orchard_nullifier`.
60    pub fn contains_orchard_nullifier(&self, orchard_nullifier: &orchard::Nullifier) -> bool {
61        let orchard_nullifiers = self.db.cf_handle("orchard_nullifiers").unwrap();
62        self.db.zs_contains(&orchard_nullifiers, &orchard_nullifier)
63    }
64
65    /// Returns `true` if the finalized state contains `ironwood_nullifier`.
66    pub fn contains_ironwood_nullifier(&self, ironwood_nullifier: &ironwood::Nullifier) -> bool {
67        let ironwood_nullifiers = self.db.cf_handle("ironwood_nullifiers").unwrap();
68        self.db
69            .zs_contains(&ironwood_nullifiers, &ironwood_nullifier)
70    }
71
72    /// Returns the [`TransactionLocation`] of the transaction that revealed
73    /// the given [`sprout::Nullifier`], if it is revealed in the finalized state and its
74    /// spending transaction hash has been indexed.
75    #[allow(clippy::unwrap_in_result)]
76    pub fn sprout_revealing_tx_loc(
77        &self,
78        sprout_nullifier: &sprout::Nullifier,
79    ) -> Option<TransactionLocation> {
80        let sprout_nullifiers = self.db.cf_handle("sprout_nullifiers").unwrap();
81        self.db.zs_get(&sprout_nullifiers, &sprout_nullifier)?
82    }
83
84    /// Returns the [`TransactionLocation`] of the transaction that revealed
85    /// the given [`sapling::Nullifier`], if it is revealed in the finalized state and its
86    /// spending transaction hash has been indexed.
87    #[allow(clippy::unwrap_in_result)]
88    pub fn sapling_revealing_tx_loc(
89        &self,
90        sapling_nullifier: &sapling::Nullifier,
91    ) -> Option<TransactionLocation> {
92        let sapling_nullifiers = self.db.cf_handle("sapling_nullifiers").unwrap();
93        self.db.zs_get(&sapling_nullifiers, &sapling_nullifier)?
94    }
95
96    /// Returns the [`TransactionLocation`] of the transaction that revealed
97    /// the given [`orchard::Nullifier`], if it is revealed in the finalized state and its
98    /// spending transaction hash has been indexed.
99    #[allow(clippy::unwrap_in_result)]
100    pub fn orchard_revealing_tx_loc(
101        &self,
102        orchard_nullifier: &orchard::Nullifier,
103    ) -> Option<TransactionLocation> {
104        let orchard_nullifiers = self.db.cf_handle("orchard_nullifiers").unwrap();
105        self.db.zs_get(&orchard_nullifiers, &orchard_nullifier)?
106    }
107
108    /// Returns the [`TransactionLocation`] of the transaction that revealed
109    /// the given [`ironwood::Nullifier`], if it is revealed in the finalized state and its
110    /// spending transaction hash has been indexed.
111    #[allow(clippy::unwrap_in_result)]
112    pub fn ironwood_revealing_tx_loc(
113        &self,
114        ironwood_nullifier: &ironwood::Nullifier,
115    ) -> Option<TransactionLocation> {
116        let ironwood_nullifiers = self.db.cf_handle("ironwood_nullifiers").unwrap();
117        self.db.zs_get(&ironwood_nullifiers, &ironwood_nullifier)?
118    }
119
120    /// Returns `true` if the finalized state contains `sprout_anchor`.
121    #[allow(dead_code)]
122    pub fn contains_sprout_anchor(&self, sprout_anchor: &sprout::tree::Root) -> bool {
123        let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap();
124        self.db.zs_contains(&sprout_anchors, &sprout_anchor)
125    }
126
127    /// Returns `true` if the finalized state contains `sapling_anchor`.
128    pub fn contains_sapling_anchor(&self, sapling_anchor: &sapling::tree::Root) -> bool {
129        let sapling_anchors = self.db.cf_handle("sapling_anchors").unwrap();
130        self.db.zs_contains(&sapling_anchors, &sapling_anchor)
131    }
132
133    /// Returns `true` if the finalized state contains `orchard_anchor`.
134    pub fn contains_orchard_anchor(&self, orchard_anchor: &orchard::tree::Root) -> bool {
135        let orchard_anchors = self.db.cf_handle("orchard_anchors").unwrap();
136        self.db.zs_contains(&orchard_anchors, &orchard_anchor)
137    }
138
139    /// Returns `true` if the finalized state contains `ironwood_anchor`.
140    ///
141    /// Ironwood reuses the Orchard tree root type, but anchors live in their own column family.
142    pub fn contains_ironwood_anchor(&self, ironwood_anchor: &orchard::tree::Root) -> bool {
143        let ironwood_anchors = self.db.cf_handle("ironwood_anchors").unwrap();
144        self.db.zs_contains(&ironwood_anchors, &ironwood_anchor)
145    }
146
147    // # Sprout trees
148
149    /// Returns the Sprout note commitment tree of the finalized tip
150    /// or the empty tree if the state is empty.
151    pub fn sprout_tree_for_tip(&self) -> Arc<sprout::tree::NoteCommitmentTree> {
152        if self.is_empty() {
153            return Arc::<sprout::tree::NoteCommitmentTree>::default();
154        }
155
156        let sprout_tree_cf = self.db.cf_handle("sprout_note_commitment_tree").unwrap();
157
158        // # Backwards Compatibility
159        //
160        // This code can read the column family format in 1.2.0 and earlier (tip height key),
161        // and after PR #7392 is merged (empty key). The height-based code can be removed when
162        // versions 1.2.0 and earlier are no longer supported.
163        //
164        // # Concurrency
165        //
166        // There is only one entry in this column family, which is atomically updated by a block
167        // write batch (database transaction). If we used a height as the column family tree,
168        // any updates between reading the tip height and reading the tree could cause panics.
169        //
170        // So we use the empty key `()`. Since the key has a constant value, we will always read
171        // the latest tree.
172        let mut sprout_tree: Option<Arc<sprout::tree::NoteCommitmentTree>> =
173            self.db.zs_get(&sprout_tree_cf, &());
174
175        if sprout_tree.is_none() {
176            // In Zebra 1.4.0 and later, we don't update the sprout tip tree unless it is changed.
177            // And we write with a `()` key, not a height key.
178            // So we need to look for the most recent update height if the `()` key has never been written.
179            sprout_tree = self
180                .db
181                .zs_last_key_value(&sprout_tree_cf)
182                .map(|(_key, tree_value): (Height, _)| tree_value);
183        }
184
185        sprout_tree.expect("Sprout note commitment tree must exist if there is a finalized tip")
186    }
187
188    /// Returns the Sprout note commitment tree matching the given anchor.
189    ///
190    /// This is used for interstitial tree building, which is unique to Sprout.
191    #[allow(clippy::unwrap_in_result)]
192    pub fn sprout_tree_by_anchor(
193        &self,
194        sprout_anchor: &sprout::tree::Root,
195    ) -> Option<Arc<sprout::tree::NoteCommitmentTree>> {
196        let sprout_anchors_handle = self.db.cf_handle("sprout_anchors").unwrap();
197
198        self.db
199            .zs_get(&sprout_anchors_handle, sprout_anchor)
200            .map(Arc::new)
201    }
202
203    /// Returns all the Sprout note commitment trees in the database.
204    ///
205    /// Calling this method can load a lot of data into RAM, and delay block commit transactions.
206    #[allow(dead_code)]
207    pub fn sprout_trees_full_map(
208        &self,
209    ) -> HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>> {
210        let sprout_anchors_handle = self.db.cf_handle("sprout_anchors").unwrap();
211
212        self.db
213            .zs_items_in_range_unordered(&sprout_anchors_handle, ..)
214    }
215
216    /// Returns all the Sprout note commitment tip trees.
217    /// We only store the sprout tree for the tip, so this method is mainly used in tests.
218    pub fn sprout_trees_full_tip(
219        &self,
220    ) -> impl Iterator<Item = (RawBytes, Arc<sprout::tree::NoteCommitmentTree>)> + '_ {
221        let sprout_trees = self.db.cf_handle("sprout_note_commitment_tree").unwrap();
222        self.db.zs_forward_range_iter(&sprout_trees, ..)
223    }
224
225    // # Sapling trees
226
227    /// Returns the Sapling note commitment tree of the finalized tip or the empty tree if the state
228    /// is empty.
229    pub fn sapling_tree_for_tip(&self) -> Arc<sapling::tree::NoteCommitmentTree> {
230        let height = match self.finalized_tip_height() {
231            Some(h) => h,
232            None => return Default::default(),
233        };
234
235        self.sapling_tree_by_height(&height)
236            .expect("Sapling note commitment tree must exist if there is a finalized tip")
237    }
238
239    /// Returns the Sapling note commitment tree matching the given block height, or `None` if the
240    /// height is above the finalized tip.
241    #[allow(clippy::unwrap_in_result)]
242    pub fn sapling_tree_by_height(
243        &self,
244        height: &Height,
245    ) -> Option<Arc<sapling::tree::NoteCommitmentTree>> {
246        let tip_height = self.finalized_tip_height()?;
247
248        // If we're above the tip, searching backwards would always return the tip tree.
249        // But the correct answer is "we don't know that tree yet".
250        if *height > tip_height {
251            return None;
252        }
253
254        let sapling_trees = self.db.cf_handle("sapling_note_commitment_tree").unwrap();
255
256        // If we know there must be a tree, search backwards for it.
257        let (_first_duplicate_height, tree) = self
258            .db
259            .zs_prev_key_value_back_from(&sapling_trees, height)
260            .expect(
261                "Sapling note commitment trees must exist for all heights below the finalized tip",
262            );
263
264        Some(Arc::new(tree))
265    }
266
267    /// Returns the Sapling note commitment trees in the supplied range, in increasing height order.
268    pub fn sapling_tree_by_height_range<R>(
269        &self,
270        range: R,
271    ) -> impl Iterator<Item = (Height, Arc<sapling::tree::NoteCommitmentTree>)> + '_
272    where
273        R: std::ops::RangeBounds<Height>,
274    {
275        let sapling_trees = self.db.cf_handle("sapling_note_commitment_tree").unwrap();
276        self.db.zs_forward_range_iter(&sapling_trees, range)
277    }
278
279    /// Returns the Sapling note commitment trees in the reversed range, in decreasing height order.
280    pub fn sapling_tree_by_reversed_height_range<R>(
281        &self,
282        range: R,
283    ) -> impl Iterator<Item = (Height, Arc<sapling::tree::NoteCommitmentTree>)> + '_
284    where
285        R: std::ops::RangeBounds<Height>,
286    {
287        let sapling_trees = self.db.cf_handle("sapling_note_commitment_tree").unwrap();
288        self.db.zs_reverse_range_iter(&sapling_trees, range)
289    }
290
291    /// Returns the Sapling note commitment subtree at this `index`.
292    ///
293    /// # Correctness
294    ///
295    /// This method should not be used to get subtrees for RPC responses,
296    /// because those subtree lists require that the start subtree is present in the list.
297    /// Instead, use `sapling_subtree_list_by_index_for_rpc()`.
298    #[allow(clippy::unwrap_in_result)]
299    pub(in super::super) fn sapling_subtree_by_index(
300        &self,
301        index: impl Into<NoteCommitmentSubtreeIndex> + Copy,
302    ) -> Option<NoteCommitmentSubtree<sapling_crypto::Node>> {
303        let sapling_subtrees = self
304            .db
305            .cf_handle("sapling_note_commitment_subtree")
306            .unwrap();
307
308        let subtree_data: NoteCommitmentSubtreeData<sapling_crypto::Node> =
309            self.db.zs_get(&sapling_subtrees, &index.into())?;
310
311        Some(subtree_data.with_index(index))
312    }
313
314    /// Returns a list of Sapling [`NoteCommitmentSubtree`]s in the provided range.
315    #[allow(clippy::unwrap_in_result)]
316    pub fn sapling_subtree_list_by_index_range(
317        &self,
318        range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex>,
319    ) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<sapling_crypto::Node>> {
320        let sapling_subtrees = self
321            .db
322            .cf_handle("sapling_note_commitment_subtree")
323            .unwrap();
324
325        self.db
326            .zs_forward_range_iter(&sapling_subtrees, range)
327            .collect()
328    }
329
330    /// Get the sapling note commitment subtress for the finalized tip.
331    #[allow(clippy::unwrap_in_result)]
332    fn sapling_subtree_for_tip(&self) -> Option<NoteCommitmentSubtree<sapling_crypto::Node>> {
333        let sapling_subtrees = self
334            .db
335            .cf_handle("sapling_note_commitment_subtree")
336            .unwrap();
337
338        let (index, subtree_data): (
339            NoteCommitmentSubtreeIndex,
340            NoteCommitmentSubtreeData<sapling_crypto::Node>,
341        ) = self.db.zs_last_key_value(&sapling_subtrees)?;
342
343        let tip_height = self.finalized_tip_height()?;
344        if subtree_data.end_height != tip_height {
345            return None;
346        }
347
348        Some(subtree_data.with_index(index))
349    }
350
351    // Orchard trees
352
353    /// Returns the Orchard note commitment tree of the finalized tip or the empty tree if the state
354    /// is empty.
355    pub fn orchard_tree_for_tip(&self) -> Arc<orchard::tree::NoteCommitmentTree> {
356        let height = match self.finalized_tip_height() {
357            Some(h) => h,
358            None => return Default::default(),
359        };
360
361        self.orchard_tree_by_height(&height)
362            .expect("Orchard note commitment tree must exist if there is a finalized tip")
363    }
364
365    /// Returns the Orchard note commitment tree matching the given block height,
366    /// or `None` if the height is above the finalized tip.
367    #[allow(clippy::unwrap_in_result)]
368    pub fn orchard_tree_by_height(
369        &self,
370        height: &Height,
371    ) -> Option<Arc<orchard::tree::NoteCommitmentTree>> {
372        let tip_height = self.finalized_tip_height()?;
373
374        // If we're above the tip, searching backwards would always return the tip tree.
375        // But the correct answer is "we don't know that tree yet".
376        if *height > tip_height {
377            return None;
378        }
379
380        let orchard_trees = self.db.cf_handle("orchard_note_commitment_tree").unwrap();
381
382        // If we know there must be a tree, search backwards for it.
383        let (_first_duplicate_height, tree) = self
384            .db
385            .zs_prev_key_value_back_from(&orchard_trees, height)
386            .expect(
387                "Orchard note commitment trees must exist for all heights below the finalized tip",
388            );
389
390        Some(Arc::new(tree))
391    }
392
393    /// Returns the note commitment trees in `cf` in the supplied range, in increasing height
394    /// order.
395    ///
396    /// Shared by the Orchard and Ironwood accessors, which reuse the `orchard::tree` types and
397    /// differ only in the column family.
398    fn tree_by_height_range<R>(
399        &self,
400        cf: &str,
401        range: R,
402    ) -> impl Iterator<Item = (Height, Arc<orchard::tree::NoteCommitmentTree>)> + '_
403    where
404        R: std::ops::RangeBounds<Height>,
405    {
406        let trees = self.db.cf_handle(cf).unwrap();
407        self.db.zs_forward_range_iter(&trees, range)
408    }
409
410    /// Returns a list of the note commitment subtrees in `cf` in the provided range.
411    fn subtree_list_by_index_range(
412        &self,
413        cf: &str,
414        range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex>,
415    ) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>> {
416        let subtrees = self.db.cf_handle(cf).unwrap();
417        self.db.zs_forward_range_iter(&subtrees, range).collect()
418    }
419
420    /// Returns the note commitment subtree in `cf` that is finalizing in the tip, or `None`.
421    #[allow(clippy::unwrap_in_result)]
422    fn subtree_for_tip(&self, cf: &str) -> Option<NoteCommitmentSubtree<orchard::tree::Node>> {
423        let subtrees = self.db.cf_handle(cf).unwrap();
424
425        let (index, subtree_data): (
426            NoteCommitmentSubtreeIndex,
427            NoteCommitmentSubtreeData<orchard::tree::Node>,
428        ) = self.db.zs_last_key_value(&subtrees)?;
429
430        let tip_height = self.finalized_tip_height()?;
431        if subtree_data.end_height != tip_height {
432            return None;
433        }
434
435        Some(subtree_data.with_index(index))
436    }
437
438    /// Returns the Orchard note commitment trees in the supplied range, in increasing height order.
439    pub fn orchard_tree_by_height_range<R>(
440        &self,
441        range: R,
442    ) -> impl Iterator<Item = (Height, Arc<orchard::tree::NoteCommitmentTree>)> + '_
443    where
444        R: std::ops::RangeBounds<Height>,
445    {
446        self.tree_by_height_range("orchard_note_commitment_tree", range)
447    }
448
449    /// Returns the Orchard note commitment trees in the reversed range, in decreasing height order.
450    pub fn orchard_tree_by_reversed_height_range<R>(
451        &self,
452        range: R,
453    ) -> impl Iterator<Item = (Height, Arc<orchard::tree::NoteCommitmentTree>)> + '_
454    where
455        R: std::ops::RangeBounds<Height>,
456    {
457        let orchard_trees = self.db.cf_handle("orchard_note_commitment_tree").unwrap();
458        self.db.zs_reverse_range_iter(&orchard_trees, range)
459    }
460
461    /// Returns the Orchard note commitment subtree at this `index`.
462    ///
463    /// # Correctness
464    ///
465    /// This method should not be used to get subtrees for RPC responses,
466    /// because those subtree lists require that the start subtree is present in the list.
467    /// Instead, use `orchard_subtree_list_by_index_for_rpc()`.
468    #[allow(clippy::unwrap_in_result)]
469    pub(in super::super) fn orchard_subtree_by_index(
470        &self,
471        index: impl Into<NoteCommitmentSubtreeIndex> + Copy,
472    ) -> Option<NoteCommitmentSubtree<orchard::tree::Node>> {
473        let orchard_subtrees = self
474            .db
475            .cf_handle("orchard_note_commitment_subtree")
476            .unwrap();
477
478        let subtree_data: NoteCommitmentSubtreeData<orchard::tree::Node> =
479            self.db.zs_get(&orchard_subtrees, &index.into())?;
480
481        Some(subtree_data.with_index(index))
482    }
483
484    /// Returns a list of Orchard [`NoteCommitmentSubtree`]s in the provided range.
485    pub fn orchard_subtree_list_by_index_range(
486        &self,
487        range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex>,
488    ) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>> {
489        self.subtree_list_by_index_range("orchard_note_commitment_subtree", range)
490    }
491
492    /// Get the orchard note commitment subtress for the finalized tip.
493    fn orchard_subtree_for_tip(&self) -> Option<NoteCommitmentSubtree<orchard::tree::Node>> {
494        self.subtree_for_tip("orchard_note_commitment_subtree")
495    }
496
497    // Ironwood trees
498    //
499    // Ironwood reuses the Orchard note commitment tree types (`orchard::tree::*`), but commits into
500    // its own column families. These mirror the Orchard tree accessors above.
501
502    /// Returns the Ironwood note commitment tree of the finalized tip or the empty tree if the
503    /// state is empty.
504    pub fn ironwood_tree_for_tip(&self) -> Arc<orchard::tree::NoteCommitmentTree> {
505        let height = match self.finalized_tip_height() {
506            Some(h) => h,
507            None => return Default::default(),
508        };
509
510        self.ironwood_tree_by_height(&height)
511            .expect("Ironwood note commitment tree must exist if there is a finalized tip")
512    }
513
514    /// Returns the Ironwood note commitment tree matching the given block height,
515    /// or `None` if the height is above the finalized tip.
516    #[allow(clippy::unwrap_in_result)]
517    pub fn ironwood_tree_by_height(
518        &self,
519        height: &Height,
520    ) -> Option<Arc<orchard::tree::NoteCommitmentTree>> {
521        let tip_height = self.finalized_tip_height()?;
522
523        // If we're above the tip, searching backwards would always return the tip tree.
524        // But the correct answer is "we don't know that tree yet".
525        if *height > tip_height {
526            return None;
527        }
528
529        let ironwood_trees = self.db.cf_handle("ironwood_note_commitment_tree").unwrap();
530
531        // The genesis Ironwood tree is seeded with the empty tree by the `add_ironwood_tree` format
532        // upgrade and then written per-height by block commits, so once the upgrade has run there is
533        // always a tree at or below any height up to the tip. The upgrade runs on a background
534        // thread (`spawn_format_change`), so there is a brief window right after a v27->v28 upgrade
535        // where the column family is still empty while block commits proceed. Any read during that
536        // window is at a pre-NU6.3 height — an upgrading node's chain is entirely pre-NU6.3, and no
537        // NU6.3 block can be committed before the tree is seeded — where the empty tree is the
538        // correct answer. So before activation, fall back to the empty tree rather than panicking.
539        // (This also covers networks where NU6.3 is not scheduled, whose whole chain is
540        // pre-Ironwood.) From NU6.3 activation onward a missing tree is a real invariant violation
541        // (corruption or an interrupted backfill), so fail loudly instead of masking it.
542        let nu6_3_activated = NetworkUpgrade::Nu6_3
543            .activation_height(&self.network())
544            .is_some_and(|activation| *height >= activation);
545
546        // Search backwards for the tree.
547        match self.db.zs_prev_key_value_back_from(&ironwood_trees, height) {
548            Some((_first_duplicate_height, tree)) => Some(Arc::new(tree)),
549            None if !nu6_3_activated => Some(Default::default()),
550            None => panic!(
551                "Ironwood note commitment tree must exist for all heights at or below the finalized \
552                 tip from NU6.3 activation onward"
553            ),
554        }
555    }
556
557    /// Returns the Ironwood note commitment trees in the supplied range, in increasing height order.
558    pub fn ironwood_tree_by_height_range<R>(
559        &self,
560        range: R,
561    ) -> impl Iterator<Item = (Height, Arc<orchard::tree::NoteCommitmentTree>)> + '_
562    where
563        R: std::ops::RangeBounds<Height>,
564    {
565        self.tree_by_height_range("ironwood_note_commitment_tree", range)
566    }
567
568    /// Returns a list of Ironwood [`NoteCommitmentSubtree`]s in the provided range.
569    pub fn ironwood_subtree_list_by_index_range(
570        &self,
571        range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex>,
572    ) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>> {
573        self.subtree_list_by_index_range("ironwood_note_commitment_subtree", range)
574    }
575
576    /// Get the Ironwood note commitment subtree for the finalized tip.
577    fn ironwood_subtree_for_tip(&self) -> Option<NoteCommitmentSubtree<orchard::tree::Node>> {
578        self.subtree_for_tip("ironwood_note_commitment_subtree")
579    }
580
581    /// Returns the shielded note commitment trees of the finalized tip
582    /// or the empty trees if the state is empty.
583    /// Additionally, returns the sapling and orchard subtrees for the finalized tip if
584    /// the current subtree is finalizing in the tip, None otherwise.
585    pub fn note_commitment_trees_for_tip(&self) -> NoteCommitmentTrees {
586        NoteCommitmentTrees {
587            sprout: self.sprout_tree_for_tip(),
588            sapling: self.sapling_tree_for_tip(),
589            sapling_subtree: self.sapling_subtree_for_tip(),
590            orchard: self.orchard_tree_for_tip(),
591            orchard_subtree: self.orchard_subtree_for_tip(),
592            ironwood: self.ironwood_tree_for_tip(),
593            ironwood_subtree: self.ironwood_subtree_for_tip(),
594        }
595    }
596}
597
598impl DiskWriteBatch {
599    /// Prepare a database batch containing `finalized.block`'s shielded transaction indexes,
600    /// and return it (without actually writing anything).
601    ///
602    /// If this method returns an error, it will be propagated,
603    /// and the batch should not be written to the database.
604    pub fn prepare_shielded_transaction_batch(
605        &mut self,
606        zebra_db: &ZebraDb,
607        finalized: &FinalizedBlock,
608    ) {
609        #[cfg(feature = "indexer")]
610        let FinalizedBlock { block, height, .. } = finalized;
611
612        // Index each transaction's shielded data
613        #[cfg(feature = "indexer")]
614        for (tx_index, transaction) in block.transactions.iter().enumerate() {
615            let tx_loc = TransactionLocation::from_usize(*height, tx_index);
616            self.prepare_nullifier_batch(zebra_db, transaction, tx_loc);
617        }
618
619        #[cfg(not(feature = "indexer"))]
620        for transaction in &finalized.block.transactions {
621            self.prepare_nullifier_batch(zebra_db, transaction);
622        }
623    }
624
625    /// Prepare a database batch containing `finalized.block`'s nullifiers,
626    /// and return it (without actually writing anything).
627    ///
628    /// # Errors
629    ///
630    /// - This method doesn't currently return any errors, but it might in future
631    #[allow(clippy::unwrap_in_result)]
632    pub fn prepare_nullifier_batch(
633        &mut self,
634        zebra_db: &ZebraDb,
635        transaction: &Transaction,
636        #[cfg(feature = "indexer")] transaction_location: TransactionLocation,
637    ) {
638        let db = &zebra_db.db;
639        let sprout_nullifiers = db.cf_handle("sprout_nullifiers").unwrap();
640        let sapling_nullifiers = db.cf_handle("sapling_nullifiers").unwrap();
641        let orchard_nullifiers = db.cf_handle("orchard_nullifiers").unwrap();
642        let ironwood_nullifiers = db.cf_handle("ironwood_nullifiers").unwrap();
643
644        #[cfg(feature = "indexer")]
645        let insert_value = transaction_location;
646        #[cfg(not(feature = "indexer"))]
647        let insert_value = ();
648
649        // Mark sprout, sapling, orchard, and ironwood nullifiers as spent
650        for sprout_nullifier in transaction.sprout_nullifiers() {
651            self.zs_insert(&sprout_nullifiers, sprout_nullifier, insert_value);
652        }
653        for sapling_nullifier in transaction.sapling_nullifiers() {
654            self.zs_insert(&sapling_nullifiers, sapling_nullifier, insert_value);
655        }
656        for orchard_nullifier in transaction.orchard_nullifiers() {
657            self.zs_insert(&orchard_nullifiers, orchard_nullifier, insert_value);
658        }
659        for ironwood_nullifier in transaction.ironwood_nullifiers() {
660            self.zs_insert(&ironwood_nullifiers, ironwood_nullifier, insert_value);
661        }
662    }
663
664    /// Prepare a database batch containing the note commitment and history tree updates
665    /// from `finalized.block`, and return it (without actually writing anything).
666    ///
667    /// If this method returns an error, it will be propagated,
668    /// and the batch should not be written to the database.
669    #[allow(clippy::unwrap_in_result)]
670    pub fn prepare_trees_batch(
671        &mut self,
672        zebra_db: &ZebraDb,
673        finalized: &FinalizedBlock,
674        prev_note_commitment_trees: Option<NoteCommitmentTrees>,
675    ) {
676        let FinalizedBlock {
677            height,
678            treestate:
679                Treestate {
680                    note_commitment_trees,
681                    history_tree,
682                },
683            ..
684        } = finalized;
685
686        let prev_sprout_tree = prev_note_commitment_trees.as_ref().map_or_else(
687            || zebra_db.sprout_tree_for_tip(),
688            |prev_trees| prev_trees.sprout.clone(),
689        );
690        let prev_sapling_tree = prev_note_commitment_trees.as_ref().map_or_else(
691            || zebra_db.sapling_tree_for_tip(),
692            |prev_trees| prev_trees.sapling.clone(),
693        );
694        let prev_orchard_tree = prev_note_commitment_trees.as_ref().map_or_else(
695            || zebra_db.orchard_tree_for_tip(),
696            |prev_trees| prev_trees.orchard.clone(),
697        );
698        let prev_ironwood_tree = prev_note_commitment_trees.as_ref().map_or_else(
699            || zebra_db.ironwood_tree_for_tip(),
700            |prev_trees| prev_trees.ironwood.clone(),
701        );
702
703        // Update the Sprout tree and store its anchor only if it has changed
704        if height.is_min() || prev_sprout_tree != note_commitment_trees.sprout {
705            self.update_sprout_tree(zebra_db, &note_commitment_trees.sprout)
706        }
707
708        // Store the Sapling tree, anchor, and any new subtrees only if they have changed
709        if height.is_min() || prev_sapling_tree != note_commitment_trees.sapling {
710            self.create_sapling_tree(zebra_db, height, &note_commitment_trees.sapling);
711
712            if let Some(subtree) = note_commitment_trees.sapling_subtree {
713                self.insert_sapling_subtree(zebra_db, &subtree);
714            }
715        }
716
717        // Store the Orchard tree, anchor, and any new subtrees only if they have changed
718        if height.is_min() || prev_orchard_tree != note_commitment_trees.orchard {
719            self.create_orchard_tree(zebra_db, height, &note_commitment_trees.orchard);
720
721            if let Some(subtree) = note_commitment_trees.orchard_subtree {
722                self.insert_orchard_subtree(zebra_db, &subtree);
723            }
724        }
725
726        // Store the Ironwood tree, anchor, and any new subtrees only if they have changed
727        if height.is_min() || prev_ironwood_tree != note_commitment_trees.ironwood {
728            self.create_ironwood_tree(zebra_db, height, &note_commitment_trees.ironwood);
729
730            if let Some(subtree) = note_commitment_trees.ironwood_subtree {
731                self.insert_ironwood_subtree(zebra_db, &subtree);
732            }
733        }
734
735        self.update_history_tree(zebra_db, history_tree);
736    }
737
738    // Sprout tree methods
739
740    /// Updates the Sprout note commitment tree for the tip, and the Sprout anchors.
741    pub fn update_sprout_tree(
742        &mut self,
743        zebra_db: &ZebraDb,
744        tree: &sprout::tree::NoteCommitmentTree,
745    ) {
746        let sprout_anchors = zebra_db.db.cf_handle("sprout_anchors").unwrap();
747        let sprout_tree_cf = zebra_db
748            .db
749            .cf_handle("sprout_note_commitment_tree")
750            .unwrap();
751
752        // Sprout lookups need all previous trees by their anchors.
753        // The root must be calculated first, so it is cached in the database.
754        self.zs_insert(&sprout_anchors, tree.root(), tree);
755        self.zs_insert(&sprout_tree_cf, (), tree);
756    }
757
758    /// Legacy method: Deletes the range of Sprout note commitment trees at the given [`Height`]s.
759    /// Doesn't delete anchors from the anchor index. Doesn't delete the upper bound.
760    ///
761    /// From state format 25.3.0 onwards, the Sprout trees are indexed by an empty key,
762    /// so this method does nothing.
763    pub fn delete_range_sprout_tree(&mut self, zebra_db: &ZebraDb, from: &Height, to: &Height) {
764        let sprout_tree_cf = zebra_db
765            .db
766            .cf_handle("sprout_note_commitment_tree")
767            .unwrap();
768
769        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
770        self.zs_delete_range(&sprout_tree_cf, from, to);
771    }
772
773    /// Deletes the given Sprout note commitment tree `anchor`.
774    #[allow(dead_code)]
775    pub fn delete_sprout_anchor(&mut self, zebra_db: &ZebraDb, anchor: &sprout::tree::Root) {
776        let sprout_anchors = zebra_db.db.cf_handle("sprout_anchors").unwrap();
777        self.zs_delete(&sprout_anchors, anchor);
778    }
779
780    // Sapling tree methods
781
782    /// Inserts or overwrites the Sapling note commitment tree at the given [`Height`],
783    /// and the Sapling anchors.
784    pub fn create_sapling_tree(
785        &mut self,
786        zebra_db: &ZebraDb,
787        height: &Height,
788        tree: &sapling::tree::NoteCommitmentTree,
789    ) {
790        let sapling_anchors = zebra_db.db.cf_handle("sapling_anchors").unwrap();
791        let sapling_tree_cf = zebra_db
792            .db
793            .cf_handle("sapling_note_commitment_tree")
794            .unwrap();
795
796        self.zs_insert(&sapling_anchors, tree.root(), ());
797        self.zs_insert(&sapling_tree_cf, height, tree);
798    }
799
800    /// Inserts the Sapling note commitment subtree into the batch.
801    pub fn insert_sapling_subtree(
802        &mut self,
803        zebra_db: &ZebraDb,
804        subtree: &NoteCommitmentSubtree<sapling_crypto::Node>,
805    ) {
806        let sapling_subtree_cf = zebra_db
807            .db
808            .cf_handle("sapling_note_commitment_subtree")
809            .unwrap();
810        self.zs_insert(&sapling_subtree_cf, subtree.index, subtree.into_data());
811    }
812
813    /// Deletes the Sapling note commitment tree at the given [`Height`].
814    pub fn delete_sapling_tree(&mut self, zebra_db: &ZebraDb, height: &Height) {
815        let sapling_tree_cf = zebra_db
816            .db
817            .cf_handle("sapling_note_commitment_tree")
818            .unwrap();
819        self.zs_delete(&sapling_tree_cf, height);
820    }
821
822    /// Deletes the range of Sapling note commitment trees at the given [`Height`]s.
823    /// Doesn't delete anchors from the anchor index. Doesn't delete the upper bound.
824    #[allow(dead_code)]
825    pub fn delete_range_sapling_tree(&mut self, zebra_db: &ZebraDb, from: &Height, to: &Height) {
826        let sapling_tree_cf = zebra_db
827            .db
828            .cf_handle("sapling_note_commitment_tree")
829            .unwrap();
830
831        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
832        self.zs_delete_range(&sapling_tree_cf, from, to);
833    }
834
835    /// Deletes the given Sapling note commitment tree `anchor`.
836    #[allow(dead_code)]
837    pub fn delete_sapling_anchor(&mut self, zebra_db: &ZebraDb, anchor: &sapling::tree::Root) {
838        let sapling_anchors = zebra_db.db.cf_handle("sapling_anchors").unwrap();
839        self.zs_delete(&sapling_anchors, anchor);
840    }
841
842    /// Deletes the range of Sapling subtrees at the given [`NoteCommitmentSubtreeIndex`]es.
843    /// Doesn't delete the upper bound.
844    pub fn delete_range_sapling_subtree(
845        &mut self,
846        zebra_db: &ZebraDb,
847        from: NoteCommitmentSubtreeIndex,
848        to: NoteCommitmentSubtreeIndex,
849    ) {
850        let sapling_subtree_cf = zebra_db
851            .db
852            .cf_handle("sapling_note_commitment_subtree")
853            .unwrap();
854
855        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
856        self.zs_delete_range(&sapling_subtree_cf, from, to);
857    }
858
859    // Orchard tree methods
860
861    /// Inserts or overwrites the Orchard note commitment tree at the given [`Height`],
862    /// and the Orchard anchors.
863    pub fn create_orchard_tree(
864        &mut self,
865        zebra_db: &ZebraDb,
866        height: &Height,
867        tree: &orchard::tree::NoteCommitmentTree,
868    ) {
869        self.create_note_commitment_tree(
870            zebra_db,
871            "orchard_anchors",
872            "orchard_note_commitment_tree",
873            height,
874            tree,
875        );
876    }
877
878    /// Inserts the Orchard note commitment subtree into the batch.
879    pub fn insert_orchard_subtree(
880        &mut self,
881        zebra_db: &ZebraDb,
882        subtree: &NoteCommitmentSubtree<orchard::tree::Node>,
883    ) {
884        self.insert_note_commitment_subtree(zebra_db, "orchard_note_commitment_subtree", subtree);
885    }
886
887    // Ironwood tree methods (Ironwood reuses the Orchard tree types, in its own column families)
888
889    /// Inserts or overwrites the Ironwood note commitment tree at the given [`Height`],
890    /// and the Ironwood anchors.
891    pub fn create_ironwood_tree(
892        &mut self,
893        zebra_db: &ZebraDb,
894        height: &Height,
895        tree: &orchard::tree::NoteCommitmentTree,
896    ) {
897        self.create_note_commitment_tree(
898            zebra_db,
899            "ironwood_anchors",
900            "ironwood_note_commitment_tree",
901            height,
902            tree,
903        );
904    }
905
906    /// Inserts the Ironwood note commitment subtree into the batch.
907    pub fn insert_ironwood_subtree(
908        &mut self,
909        zebra_db: &ZebraDb,
910        subtree: &NoteCommitmentSubtree<orchard::tree::Node>,
911    ) {
912        self.insert_note_commitment_subtree(zebra_db, "ironwood_note_commitment_subtree", subtree);
913    }
914
915    /// Inserts a note commitment `tree` at `height` into `tree_cf`, and its root into `anchors_cf`.
916    ///
917    /// Shared by the Orchard and Ironwood pools, which reuse the `orchard::tree` types and differ
918    /// only in the column families.
919    fn create_note_commitment_tree(
920        &mut self,
921        zebra_db: &ZebraDb,
922        anchors_cf: &str,
923        tree_cf: &str,
924        height: &Height,
925        tree: &orchard::tree::NoteCommitmentTree,
926    ) {
927        let anchors = zebra_db.db.cf_handle(anchors_cf).unwrap();
928        let tree_cf = zebra_db.db.cf_handle(tree_cf).unwrap();
929
930        self.zs_insert(&anchors, tree.root(), ());
931        self.zs_insert(&tree_cf, height, tree);
932    }
933
934    /// Inserts a note commitment `subtree` into `subtree_cf`.
935    fn insert_note_commitment_subtree(
936        &mut self,
937        zebra_db: &ZebraDb,
938        subtree_cf: &str,
939        subtree: &NoteCommitmentSubtree<orchard::tree::Node>,
940    ) {
941        let subtree_cf = zebra_db.db.cf_handle(subtree_cf).unwrap();
942        self.zs_insert(&subtree_cf, subtree.index, subtree.into_data());
943    }
944
945    /// Deletes the Orchard note commitment tree at the given [`Height`].
946    pub fn delete_orchard_tree(&mut self, zebra_db: &ZebraDb, height: &Height) {
947        let orchard_tree_cf = zebra_db
948            .db
949            .cf_handle("orchard_note_commitment_tree")
950            .unwrap();
951        self.zs_delete(&orchard_tree_cf, height);
952    }
953
954    /// Deletes the range of Orchard note commitment trees at the given [`Height`]s.
955    /// Doesn't delete anchors from the anchor index. Doesn't delete the upper bound.
956    #[allow(dead_code)]
957    pub fn delete_range_orchard_tree(&mut self, zebra_db: &ZebraDb, from: &Height, to: &Height) {
958        let orchard_tree_cf = zebra_db
959            .db
960            .cf_handle("orchard_note_commitment_tree")
961            .unwrap();
962
963        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
964        self.zs_delete_range(&orchard_tree_cf, from, to);
965    }
966
967    /// Deletes the given Orchard note commitment tree `anchor`.
968    #[allow(dead_code)]
969    pub fn delete_orchard_anchor(&mut self, zebra_db: &ZebraDb, anchor: &orchard::tree::Root) {
970        let orchard_anchors = zebra_db.db.cf_handle("orchard_anchors").unwrap();
971        self.zs_delete(&orchard_anchors, anchor);
972    }
973
974    /// Deletes the range of Orchard subtrees at the given [`NoteCommitmentSubtreeIndex`]es.
975    /// Doesn't delete the upper bound.
976    pub fn delete_range_orchard_subtree(
977        &mut self,
978        zebra_db: &ZebraDb,
979        from: NoteCommitmentSubtreeIndex,
980        to: NoteCommitmentSubtreeIndex,
981    ) {
982        let orchard_subtree_cf = zebra_db
983            .db
984            .cf_handle("orchard_note_commitment_subtree")
985            .unwrap();
986
987        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
988        self.zs_delete_range(&orchard_subtree_cf, from, to);
989    }
990}