Skip to main content

zebra_state/service/read/
tree.rs

1//! Reading note commitment trees.
2//!
3//! In the functions in this module:
4//!
5//! The block write task commits blocks to the finalized state before updating
6//! `chain` with a cached copy of the best non-finalized chain from
7//! `NonFinalizedState.chain_set`. Then the block commit task can commit additional blocks to
8//! the finalized state after we've cloned the `chain`.
9//!
10//! This means that some blocks can be in both:
11//! - the cached [`Chain`], and
12//! - the shared finalized [`ZebraDb`] reference.
13
14use std::{collections::BTreeMap, sync::Arc};
15
16use zebra_chain::{
17    orchard, sapling,
18    subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex},
19};
20
21use crate::{
22    service::{finalized_state::ZebraDb, non_finalized_state::Chain},
23    HashOrHeight,
24};
25
26// Doc-only items
27#[allow(unused_imports)]
28use zebra_chain::subtree::NoteCommitmentSubtree;
29
30/// Returns the Sapling
31/// [`NoteCommitmentTree`](sapling::tree::NoteCommitmentTree) specified by a
32/// hash or height, if it exists in the non-finalized `chain` or finalized `db`.
33pub fn sapling_tree<C>(
34    chain: Option<C>,
35    db: &ZebraDb,
36    hash_or_height: HashOrHeight,
37) -> Option<Arc<sapling::tree::NoteCommitmentTree>>
38where
39    C: AsRef<Chain>,
40{
41    // # Correctness
42    //
43    // Since sapling treestates are the same in the finalized and non-finalized
44    // state, we check the most efficient alternative first. (`chain` is always
45    // in memory, but `db` stores blocks on disk, with a memory cache.)
46    chain
47        .and_then(|chain| chain.as_ref().sapling_tree(hash_or_height))
48        .or_else(|| db.sapling_tree_by_hash_or_height(hash_or_height))
49}
50
51/// Returns a list of Sapling [`NoteCommitmentSubtree`]s with indexes in the provided range.
52///
53/// If there is no subtree at the first index in the range, the returned list is empty.
54/// Otherwise, subtrees are continuous up to the finalized tip.
55///
56/// See [`subtrees`] for more details.
57pub fn sapling_subtrees<C>(
58    chain: Option<C>,
59    db: &ZebraDb,
60    range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
61) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<sapling_crypto::Node>>
62where
63    C: AsRef<Chain>,
64{
65    subtrees(
66        chain,
67        range,
68        |chain, range| chain.sapling_subtrees_in_range(range),
69        |range| db.sapling_subtree_list_by_index_range(range),
70    )
71}
72
73/// Returns the Orchard
74/// [`NoteCommitmentTree`](orchard::tree::NoteCommitmentTree) specified by a
75/// hash or height, if it exists in the non-finalized `chain` or finalized `db`.
76pub fn orchard_tree<C>(
77    chain: Option<C>,
78    db: &ZebraDb,
79    hash_or_height: HashOrHeight,
80) -> Option<Arc<orchard::tree::NoteCommitmentTree>>
81where
82    C: AsRef<Chain>,
83{
84    // # Correctness
85    //
86    // Since orchard treestates are the same in the finalized and non-finalized
87    // state, we check the most efficient alternative first. (`chain` is always
88    // in memory, but `db` stores blocks on disk, with a memory cache.)
89    chain
90        .and_then(|chain| chain.as_ref().orchard_tree(hash_or_height))
91        .or_else(|| db.orchard_tree_by_hash_or_height(hash_or_height))
92}
93
94/// Returns a list of Orchard [`NoteCommitmentSubtree`]s with indexes in the provided range.
95///
96/// If there is no subtree at the first index in the range, the returned list is empty.
97/// Otherwise, subtrees are continuous up to the finalized tip.
98///
99/// See [`subtrees`] for more details.
100pub fn orchard_subtrees<C>(
101    chain: Option<C>,
102    db: &ZebraDb,
103    range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
104) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>>
105where
106    C: AsRef<Chain>,
107{
108    subtrees(
109        chain,
110        range,
111        |chain, range| chain.orchard_subtrees_in_range(range),
112        |range| db.orchard_subtree_list_by_index_range(range),
113    )
114}
115
116/// Returns the Ironwood
117/// [`NoteCommitmentTree`](orchard::tree::NoteCommitmentTree) specified by a
118/// hash or height, if it exists in the non-finalized `chain` or finalized `db`.
119///
120/// Ironwood reuses the Orchard note commitment tree type, in a separate tree.
121pub fn ironwood_tree<C>(
122    chain: Option<C>,
123    db: &ZebraDb,
124    hash_or_height: HashOrHeight,
125) -> Option<Arc<orchard::tree::NoteCommitmentTree>>
126where
127    C: AsRef<Chain>,
128{
129    // # Correctness
130    //
131    // Since Ironwood treestates are the same in the finalized and non-finalized
132    // state, we check the most efficient alternative first. (`chain` is always
133    // in memory, but `db` stores blocks on disk, with a memory cache.)
134    chain
135        .and_then(|chain| chain.as_ref().ironwood_tree(hash_or_height))
136        .or_else(|| db.ironwood_tree_by_hash_or_height(hash_or_height))
137}
138
139/// Returns a list of Ironwood [`NoteCommitmentSubtree`]s with indexes in the provided range.
140///
141/// If there is no subtree at the first index in the range, the returned list is empty.
142/// Otherwise, subtrees are continuous up to the finalized tip.
143///
144/// See [`subtrees`] for more details.
145pub fn ironwood_subtrees<C>(
146    chain: Option<C>,
147    db: &ZebraDb,
148    range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
149) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>>
150where
151    C: AsRef<Chain>,
152{
153    subtrees(
154        chain,
155        range,
156        |chain, range| chain.ironwood_subtrees_in_range(range),
157        |range| db.ironwood_subtree_list_by_index_range(range),
158    )
159}
160
161/// Returns a list of [`NoteCommitmentSubtree`]s in the provided range.
162///
163/// If there is no subtree at the first index in the range, the returned list is empty.
164/// Otherwise, subtrees are continuous up to the finalized tip.
165///
166/// Accepts a `chain` from the non-finalized state, a `range` of subtree indexes to retrieve,
167/// a `read_chain` function for retrieving the `range` of subtrees from `chain`, and
168/// a `read_disk` function for retrieving the `range` from [`ZebraDb`].
169///
170/// Returns a consistent set of subtrees for the supplied chain fork and database.
171/// Avoids reading the database if the subtrees are present in memory.
172///
173/// # Correctness
174///
175/// APIs that return single subtrees can't be used for `read_chain` and `read_disk`, because they
176/// can create an inconsistent list of subtrees after concurrent non-finalized and finalized updates.
177fn subtrees<C, Range, Node, ChainSubtreeFn, DbSubtreeFn>(
178    chain: Option<C>,
179    range: Range,
180    read_chain: ChainSubtreeFn,
181    read_disk: DbSubtreeFn,
182) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>
183where
184    C: AsRef<Chain>,
185    Node: PartialEq,
186    Range: std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
187    ChainSubtreeFn: FnOnce(
188        &Chain,
189        Range,
190    )
191        -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
192    DbSubtreeFn:
193        FnOnce(Range) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
194{
195    use std::ops::Bound::*;
196
197    let start_index = match range.start_bound().cloned() {
198        Included(start_index) => start_index,
199        Excluded(start_index) => (start_index.0 + 1).into(),
200        Unbounded => 0.into(),
201    };
202
203    // # Correctness
204    //
205    // After `chain` was cloned, the StateService can commit additional blocks to the finalized state `db`.
206    // Usually, the subtrees of these blocks are consistent. But if the `chain` is a different fork to `db`,
207    // then the trees can be inconsistent. In that case, if `chain` does not contain a subtree at the first
208    // index in the provided range, we ignore all the trees in `chain` after the first inconsistent tree,
209    // because we know they will be inconsistent as well. (It is cryptographically impossible for tree roots
210    // to be equal once the leaves have diverged.)
211
212    let results = match chain.map(|chain| read_chain(chain.as_ref(), range.clone())) {
213        Some(chain_results) if chain_results.contains_key(&start_index) => return chain_results,
214        Some(chain_results) => {
215            let mut db_results = read_disk(range);
216
217            // Check for inconsistent trees in the fork.
218            for (chain_index, chain_subtree) in chain_results {
219                // If there's no matching index, just update the list of trees.
220                let Some(db_subtree) = db_results.get(&chain_index) else {
221                    db_results.insert(chain_index, chain_subtree);
222                    continue;
223                };
224
225                // We have an outdated chain fork, so skip this subtree and all remaining subtrees.
226                if &chain_subtree != db_subtree {
227                    break;
228                }
229                // Otherwise, the subtree is already in the list, so we don't need to add it.
230            }
231
232            db_results
233        }
234        None => read_disk(range),
235    };
236
237    // Check that we got the start subtree
238    if results.contains_key(&start_index) {
239        results
240    } else {
241        BTreeMap::new()
242    }
243}
244
245/// Get the history tree of the provided chain.
246pub fn history_tree<C>(
247    chain: Option<C>,
248    db: &ZebraDb,
249    hash_or_height: HashOrHeight,
250) -> Option<Arc<zebra_chain::history_tree::HistoryTree>>
251where
252    C: AsRef<Chain>,
253{
254    chain
255        .and_then(|chain| chain.as_ref().history_tree(hash_or_height))
256        .or_else(|| Some(db.history_tree()))
257}