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    pub fn prepare_shielded_transaction_batch(
602        &mut self,
603        zebra_db: &ZebraDb,
604        finalized: &FinalizedBlock,
605    ) {
606        #[cfg(feature = "indexer")]
607        let FinalizedBlock { block, height, .. } = finalized;
608
609        // Index each transaction's shielded data
610        #[cfg(feature = "indexer")]
611        for (tx_index, transaction) in block.transactions.iter().enumerate() {
612            let tx_loc = TransactionLocation::from_usize(*height, tx_index);
613            self.prepare_nullifier_batch(zebra_db, transaction, tx_loc);
614        }
615
616        #[cfg(not(feature = "indexer"))]
617        for transaction in &finalized.block.transactions {
618            self.prepare_nullifier_batch(zebra_db, transaction);
619        }
620    }
621
622    /// Prepare a database batch containing `finalized.block`'s nullifiers,
623    /// and return it (without actually writing anything).
624    ///
625    /// # Errors
626    ///
627    /// - This method doesn't currently return any errors, but it might in future
628    #[allow(clippy::unwrap_in_result)]
629    pub fn prepare_nullifier_batch(
630        &mut self,
631        zebra_db: &ZebraDb,
632        transaction: &Transaction,
633        #[cfg(feature = "indexer")] transaction_location: TransactionLocation,
634    ) {
635        let db = &zebra_db.db;
636        let sprout_nullifiers = db.cf_handle("sprout_nullifiers").unwrap();
637        let sapling_nullifiers = db.cf_handle("sapling_nullifiers").unwrap();
638        let orchard_nullifiers = db.cf_handle("orchard_nullifiers").unwrap();
639        let ironwood_nullifiers = db.cf_handle("ironwood_nullifiers").unwrap();
640
641        #[cfg(feature = "indexer")]
642        let insert_value = transaction_location;
643        #[cfg(not(feature = "indexer"))]
644        let insert_value = ();
645
646        // Mark sprout, sapling, orchard, and ironwood nullifiers as spent
647        for nullifier in transaction.sprout_nullifiers() {
648            self.zs_insert(&sprout_nullifiers, nullifier, insert_value);
649        }
650        for nullifier in transaction.sapling_nullifiers() {
651            self.zs_insert(&sapling_nullifiers, nullifier, insert_value);
652        }
653        for nullifier in transaction.orchard_nullifiers() {
654            self.zs_insert(&orchard_nullifiers, nullifier, insert_value);
655        }
656        for ironwood_nullifier in transaction.ironwood_nullifiers() {
657            self.zs_insert(&ironwood_nullifiers, ironwood_nullifier, insert_value);
658        }
659    }
660
661    /// Prepare a database batch containing the note commitment and history tree updates
662    /// from `finalized.block`, and return it (without actually writing anything).
663    pub fn prepare_trees_batch(
664        &mut self,
665        zebra_db: &ZebraDb,
666        finalized: &FinalizedBlock,
667        prev_note_commitment_trees: Option<NoteCommitmentTrees>,
668    ) {
669        let FinalizedBlock {
670            height,
671            treestate:
672                Treestate {
673                    note_commitment_trees,
674                    history_tree,
675                },
676            ..
677        } = finalized;
678
679        let prev_sprout_tree = prev_note_commitment_trees.as_ref().map_or_else(
680            || zebra_db.sprout_tree_for_tip(),
681            |prev_trees| prev_trees.sprout.clone(),
682        );
683        let prev_sapling_tree = prev_note_commitment_trees.as_ref().map_or_else(
684            || zebra_db.sapling_tree_for_tip(),
685            |prev_trees| prev_trees.sapling.clone(),
686        );
687        let prev_orchard_tree = prev_note_commitment_trees.as_ref().map_or_else(
688            || zebra_db.orchard_tree_for_tip(),
689            |prev_trees| prev_trees.orchard.clone(),
690        );
691        let prev_ironwood_tree = prev_note_commitment_trees.as_ref().map_or_else(
692            || zebra_db.ironwood_tree_for_tip(),
693            |prev_trees| prev_trees.ironwood.clone(),
694        );
695
696        // Update the Sprout tree and store its anchor only if it has changed
697        if height.is_min() || prev_sprout_tree != note_commitment_trees.sprout {
698            self.update_sprout_tree(zebra_db, &note_commitment_trees.sprout)
699        }
700
701        // Store the Sapling tree, anchor, and any new subtrees only if they have changed
702        if height.is_min() || prev_sapling_tree != note_commitment_trees.sapling {
703            self.create_sapling_tree(zebra_db, height, &note_commitment_trees.sapling);
704
705            if let Some(subtree) = note_commitment_trees.sapling_subtree {
706                self.insert_sapling_subtree(zebra_db, &subtree);
707            }
708        }
709
710        // Store the Orchard tree, anchor, and any new subtrees only if they have changed
711        if height.is_min() || prev_orchard_tree != note_commitment_trees.orchard {
712            self.create_orchard_tree(zebra_db, height, &note_commitment_trees.orchard);
713
714            if let Some(subtree) = note_commitment_trees.orchard_subtree {
715                self.insert_orchard_subtree(zebra_db, &subtree);
716            }
717        }
718
719        // Store the Ironwood tree, anchor, and any new subtrees only if they have changed
720        if height.is_min() || prev_ironwood_tree != note_commitment_trees.ironwood {
721            self.create_ironwood_tree(zebra_db, height, &note_commitment_trees.ironwood);
722
723            if let Some(subtree) = note_commitment_trees.ironwood_subtree {
724                self.insert_ironwood_subtree(zebra_db, &subtree);
725            }
726        }
727
728        self.update_history_tree(zebra_db, history_tree);
729    }
730
731    // Sprout tree methods
732
733    /// Updates the Sprout note commitment tree for the tip, and the Sprout anchors.
734    pub fn update_sprout_tree(
735        &mut self,
736        zebra_db: &ZebraDb,
737        tree: &sprout::tree::NoteCommitmentTree,
738    ) {
739        let sprout_anchors = zebra_db.db.cf_handle("sprout_anchors").unwrap();
740        let sprout_tree_cf = zebra_db
741            .db
742            .cf_handle("sprout_note_commitment_tree")
743            .unwrap();
744
745        // Sprout lookups need all previous trees by their anchors.
746        // The root must be calculated first, so it is cached in the database.
747        self.zs_insert(&sprout_anchors, tree.root(), tree);
748        self.zs_insert(&sprout_tree_cf, (), tree);
749    }
750
751    /// Legacy method: Deletes the range of Sprout note commitment trees at the given [`Height`]s.
752    /// Doesn't delete anchors from the anchor index. Doesn't delete the upper bound.
753    ///
754    /// From state format 25.3.0 onwards, the Sprout trees are indexed by an empty key,
755    /// so this method does nothing.
756    pub fn delete_range_sprout_tree(&mut self, zebra_db: &ZebraDb, from: &Height, to: &Height) {
757        let sprout_tree_cf = zebra_db
758            .db
759            .cf_handle("sprout_note_commitment_tree")
760            .unwrap();
761
762        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
763        self.zs_delete_range(&sprout_tree_cf, from, to);
764    }
765
766    /// Deletes the given Sprout note commitment tree `anchor`.
767    #[allow(dead_code)]
768    pub fn delete_sprout_anchor(&mut self, zebra_db: &ZebraDb, anchor: &sprout::tree::Root) {
769        let sprout_anchors = zebra_db.db.cf_handle("sprout_anchors").unwrap();
770        self.zs_delete(&sprout_anchors, anchor);
771    }
772
773    // Sapling tree methods
774
775    /// Inserts or overwrites the Sapling note commitment tree at the given [`Height`],
776    /// and the Sapling anchors.
777    pub fn create_sapling_tree(
778        &mut self,
779        zebra_db: &ZebraDb,
780        height: &Height,
781        tree: &sapling::tree::NoteCommitmentTree,
782    ) {
783        let sapling_anchors = zebra_db.db.cf_handle("sapling_anchors").unwrap();
784        let sapling_tree_cf = zebra_db
785            .db
786            .cf_handle("sapling_note_commitment_tree")
787            .unwrap();
788
789        self.zs_insert(&sapling_anchors, tree.root(), ());
790        self.zs_insert(&sapling_tree_cf, height, tree);
791    }
792
793    /// Inserts the Sapling note commitment subtree into the batch.
794    pub fn insert_sapling_subtree(
795        &mut self,
796        zebra_db: &ZebraDb,
797        subtree: &NoteCommitmentSubtree<sapling_crypto::Node>,
798    ) {
799        let sapling_subtree_cf = zebra_db
800            .db
801            .cf_handle("sapling_note_commitment_subtree")
802            .unwrap();
803        self.zs_insert(&sapling_subtree_cf, subtree.index, subtree.into_data());
804    }
805
806    /// Deletes the Sapling note commitment tree at the given [`Height`].
807    pub fn delete_sapling_tree(&mut self, zebra_db: &ZebraDb, height: &Height) {
808        let sapling_tree_cf = zebra_db
809            .db
810            .cf_handle("sapling_note_commitment_tree")
811            .unwrap();
812        self.zs_delete(&sapling_tree_cf, height);
813    }
814
815    /// Deletes the range of Sapling note commitment trees at the given [`Height`]s.
816    /// Doesn't delete anchors from the anchor index. Doesn't delete the upper bound.
817    #[allow(dead_code)]
818    pub fn delete_range_sapling_tree(&mut self, zebra_db: &ZebraDb, from: &Height, to: &Height) {
819        let sapling_tree_cf = zebra_db
820            .db
821            .cf_handle("sapling_note_commitment_tree")
822            .unwrap();
823
824        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
825        self.zs_delete_range(&sapling_tree_cf, from, to);
826    }
827
828    /// Deletes the given Sapling note commitment tree `anchor`.
829    #[allow(dead_code)]
830    pub fn delete_sapling_anchor(&mut self, zebra_db: &ZebraDb, anchor: &sapling::tree::Root) {
831        let sapling_anchors = zebra_db.db.cf_handle("sapling_anchors").unwrap();
832        self.zs_delete(&sapling_anchors, anchor);
833    }
834
835    /// Deletes the range of Sapling subtrees at the given [`NoteCommitmentSubtreeIndex`]es.
836    /// Doesn't delete the upper bound.
837    pub fn delete_range_sapling_subtree(
838        &mut self,
839        zebra_db: &ZebraDb,
840        from: NoteCommitmentSubtreeIndex,
841        to: NoteCommitmentSubtreeIndex,
842    ) {
843        let sapling_subtree_cf = zebra_db
844            .db
845            .cf_handle("sapling_note_commitment_subtree")
846            .unwrap();
847
848        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
849        self.zs_delete_range(&sapling_subtree_cf, from, to);
850    }
851
852    // Orchard tree methods
853
854    /// Inserts or overwrites the Orchard note commitment tree at the given [`Height`],
855    /// and the Orchard anchors.
856    pub fn create_orchard_tree(
857        &mut self,
858        zebra_db: &ZebraDb,
859        height: &Height,
860        tree: &orchard::tree::NoteCommitmentTree,
861    ) {
862        self.create_note_commitment_tree(
863            zebra_db,
864            "orchard_anchors",
865            "orchard_note_commitment_tree",
866            height,
867            tree,
868        );
869    }
870
871    /// Inserts the Orchard note commitment subtree into the batch.
872    pub fn insert_orchard_subtree(
873        &mut self,
874        zebra_db: &ZebraDb,
875        subtree: &NoteCommitmentSubtree<orchard::tree::Node>,
876    ) {
877        self.insert_note_commitment_subtree(zebra_db, "orchard_note_commitment_subtree", subtree);
878    }
879
880    // Ironwood tree methods (Ironwood reuses the Orchard tree types, in its own column families)
881
882    /// Inserts or overwrites the Ironwood note commitment tree at the given [`Height`],
883    /// and the Ironwood anchors.
884    pub fn create_ironwood_tree(
885        &mut self,
886        zebra_db: &ZebraDb,
887        height: &Height,
888        tree: &orchard::tree::NoteCommitmentTree,
889    ) {
890        self.create_note_commitment_tree(
891            zebra_db,
892            "ironwood_anchors",
893            "ironwood_note_commitment_tree",
894            height,
895            tree,
896        );
897    }
898
899    /// Inserts the Ironwood note commitment subtree into the batch.
900    pub fn insert_ironwood_subtree(
901        &mut self,
902        zebra_db: &ZebraDb,
903        subtree: &NoteCommitmentSubtree<orchard::tree::Node>,
904    ) {
905        self.insert_note_commitment_subtree(zebra_db, "ironwood_note_commitment_subtree", subtree);
906    }
907
908    /// Inserts a note commitment `tree` at `height` into `tree_cf`, and its root into `anchors_cf`.
909    ///
910    /// Shared by the Orchard and Ironwood pools, which reuse the `orchard::tree` types and differ
911    /// only in the column families.
912    fn create_note_commitment_tree(
913        &mut self,
914        zebra_db: &ZebraDb,
915        anchors_cf: &str,
916        tree_cf: &str,
917        height: &Height,
918        tree: &orchard::tree::NoteCommitmentTree,
919    ) {
920        let anchors = zebra_db.db.cf_handle(anchors_cf).unwrap();
921        let tree_cf = zebra_db.db.cf_handle(tree_cf).unwrap();
922
923        self.zs_insert(&anchors, tree.root(), ());
924        self.zs_insert(&tree_cf, height, tree);
925    }
926
927    /// Inserts a note commitment `subtree` into `subtree_cf`.
928    fn insert_note_commitment_subtree(
929        &mut self,
930        zebra_db: &ZebraDb,
931        subtree_cf: &str,
932        subtree: &NoteCommitmentSubtree<orchard::tree::Node>,
933    ) {
934        let subtree_cf = zebra_db.db.cf_handle(subtree_cf).unwrap();
935        self.zs_insert(&subtree_cf, subtree.index, subtree.into_data());
936    }
937
938    /// Deletes the Orchard note commitment tree at the given [`Height`].
939    pub fn delete_orchard_tree(&mut self, zebra_db: &ZebraDb, height: &Height) {
940        let orchard_tree_cf = zebra_db
941            .db
942            .cf_handle("orchard_note_commitment_tree")
943            .unwrap();
944        self.zs_delete(&orchard_tree_cf, height);
945    }
946
947    /// Deletes the range of Orchard note commitment trees at the given [`Height`]s.
948    /// Doesn't delete anchors from the anchor index. Doesn't delete the upper bound.
949    #[allow(dead_code)]
950    pub fn delete_range_orchard_tree(&mut self, zebra_db: &ZebraDb, from: &Height, to: &Height) {
951        let orchard_tree_cf = zebra_db
952            .db
953            .cf_handle("orchard_note_commitment_tree")
954            .unwrap();
955
956        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
957        self.zs_delete_range(&orchard_tree_cf, from, to);
958    }
959
960    /// Deletes the given Orchard note commitment tree `anchor`.
961    #[allow(dead_code)]
962    pub fn delete_orchard_anchor(&mut self, zebra_db: &ZebraDb, anchor: &orchard::tree::Root) {
963        let orchard_anchors = zebra_db.db.cf_handle("orchard_anchors").unwrap();
964        self.zs_delete(&orchard_anchors, anchor);
965    }
966
967    /// Deletes the range of Orchard subtrees at the given [`NoteCommitmentSubtreeIndex`]es.
968    /// Doesn't delete the upper bound.
969    pub fn delete_range_orchard_subtree(
970        &mut self,
971        zebra_db: &ZebraDb,
972        from: NoteCommitmentSubtreeIndex,
973        to: NoteCommitmentSubtreeIndex,
974    ) {
975        let orchard_subtree_cf = zebra_db
976            .db
977            .cf_handle("orchard_note_commitment_subtree")
978            .unwrap();
979
980        // TODO: convert zs_delete_range() to take std::ops::RangeBounds
981        self.zs_delete_range(&orchard_subtree_cf, from, to);
982    }
983}