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    block, 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 the Sapling
52/// [`NoteCommitmentTree`](sapling::tree::NoteCommitmentTree) for the block with `hash`,
53/// if it exists in any of the non-finalized `chains` or finalized `db`.
54///
55/// Unlike [`sapling_tree`], this checks every non-finalized chain (best chain first,
56/// then side chains), so it is immune to reorgs that move a block from the best chain
57/// onto a still-retained side chain.
58pub fn any_sapling_tree<'a, C: AsRef<Chain> + 'a>(
59    mut chains: impl Iterator<Item = &'a C>,
60    db: &ZebraDb,
61    hash: block::Hash,
62) -> Option<Arc<sapling::tree::NoteCommitmentTree>> {
63    // # Correctness
64    //
65    // Lookups are by hash, not height, because the same height can have different
66    // treestates in different chain forks. A block hash identifies a single treestate,
67    // which is the same in whichever chain or the finalized state contains the block,
68    // so we check the most efficient alternative first. (`chains` are always in memory,
69    // but `db` stores blocks on disk, with a memory cache.)
70    chains
71        .find_map(|chain| chain.as_ref().sapling_tree(hash.into()))
72        .or_else(|| db.sapling_tree_by_hash_or_height(hash.into()))
73}
74
75/// Returns a list of Sapling [`NoteCommitmentSubtree`]s with indexes in the provided range.
76///
77/// If there is no subtree at the first index in the range, the returned list is empty.
78/// Otherwise, subtrees are continuous up to the finalized tip.
79///
80/// See [`subtrees`] for more details.
81pub fn sapling_subtrees<C>(
82    chain: Option<C>,
83    db: &ZebraDb,
84    range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
85) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<sapling_crypto::Node>>
86where
87    C: AsRef<Chain>,
88{
89    subtrees(
90        chain,
91        range,
92        |chain, range| chain.sapling_subtrees_in_range(range),
93        |range| db.sapling_subtree_list_by_index_range(range),
94    )
95}
96
97/// Returns the Orchard
98/// [`NoteCommitmentTree`](orchard::tree::NoteCommitmentTree) specified by a
99/// hash or height, if it exists in the non-finalized `chain` or finalized `db`.
100pub fn orchard_tree<C>(
101    chain: Option<C>,
102    db: &ZebraDb,
103    hash_or_height: HashOrHeight,
104) -> Option<Arc<orchard::tree::NoteCommitmentTree>>
105where
106    C: AsRef<Chain>,
107{
108    // # Correctness
109    //
110    // Since orchard treestates are the same in the finalized and non-finalized
111    // state, we check the most efficient alternative first. (`chain` is always
112    // in memory, but `db` stores blocks on disk, with a memory cache.)
113    chain
114        .and_then(|chain| chain.as_ref().orchard_tree(hash_or_height))
115        .or_else(|| db.orchard_tree_by_hash_or_height(hash_or_height))
116}
117
118/// Returns the Orchard
119/// [`NoteCommitmentTree`](orchard::tree::NoteCommitmentTree) for the block with `hash`,
120/// if it exists in any of the non-finalized `chains` or finalized `db`.
121///
122/// Unlike [`orchard_tree`], this checks every non-finalized chain (best chain first,
123/// then side chains), so it is immune to reorgs that move a block from the best chain
124/// onto a still-retained side chain.
125pub fn any_orchard_tree<'a, C: AsRef<Chain> + 'a>(
126    mut chains: impl Iterator<Item = &'a C>,
127    db: &ZebraDb,
128    hash: block::Hash,
129) -> Option<Arc<orchard::tree::NoteCommitmentTree>> {
130    // # Correctness
131    //
132    // Lookups are by hash, not height, because the same height can have different
133    // treestates in different chain forks. A block hash identifies a single treestate,
134    // which is the same in whichever chain or the finalized state contains the block,
135    // so we check the most efficient alternative first. (`chains` are always in memory,
136    // but `db` stores blocks on disk, with a memory cache.)
137    chains
138        .find_map(|chain| chain.as_ref().orchard_tree(hash.into()))
139        .or_else(|| db.orchard_tree_by_hash_or_height(hash.into()))
140}
141
142/// Returns a list of Orchard [`NoteCommitmentSubtree`]s with indexes in the provided range.
143///
144/// If there is no subtree at the first index in the range, the returned list is empty.
145/// Otherwise, subtrees are continuous up to the finalized tip.
146///
147/// See [`subtrees`] for more details.
148pub fn orchard_subtrees<C>(
149    chain: Option<C>,
150    db: &ZebraDb,
151    range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
152) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>>
153where
154    C: AsRef<Chain>,
155{
156    subtrees(
157        chain,
158        range,
159        |chain, range| chain.orchard_subtrees_in_range(range),
160        |range| db.orchard_subtree_list_by_index_range(range),
161    )
162}
163
164/// Returns the Ironwood
165/// [`NoteCommitmentTree`](orchard::tree::NoteCommitmentTree) specified by a
166/// hash or height, if it exists in the non-finalized `chain` or finalized `db`.
167///
168/// Ironwood reuses the Orchard note commitment tree type, in a separate tree.
169pub fn ironwood_tree<C>(
170    chain: Option<C>,
171    db: &ZebraDb,
172    hash_or_height: HashOrHeight,
173) -> Option<Arc<orchard::tree::NoteCommitmentTree>>
174where
175    C: AsRef<Chain>,
176{
177    // # Correctness
178    //
179    // Since Ironwood treestates are the same in the finalized and non-finalized
180    // state, we check the most efficient alternative first. (`chain` is always
181    // in memory, but `db` stores blocks on disk, with a memory cache.)
182    chain
183        .and_then(|chain| chain.as_ref().ironwood_tree(hash_or_height))
184        .or_else(|| db.ironwood_tree_by_hash_or_height(hash_or_height))
185}
186
187/// Returns the Ironwood
188/// [`NoteCommitmentTree`](orchard::tree::NoteCommitmentTree) for the block with `hash`,
189/// if it exists in any of the non-finalized `chains` or finalized `db`.
190///
191/// Unlike [`ironwood_tree`], this checks every non-finalized chain (best chain first,
192/// then side chains), so it is immune to reorgs that move a block from the best chain
193/// onto a still-retained side chain.
194///
195/// Ironwood reuses the Orchard note commitment tree type, in a separate tree.
196pub fn any_ironwood_tree<'a, C: AsRef<Chain> + 'a>(
197    mut chains: impl Iterator<Item = &'a C>,
198    db: &ZebraDb,
199    hash: block::Hash,
200) -> Option<Arc<orchard::tree::NoteCommitmentTree>> {
201    // # Correctness
202    //
203    // Lookups are by hash, not height, because the same height can have different
204    // treestates in different chain forks. A block hash identifies a single treestate,
205    // which is the same in whichever chain or the finalized state contains the block,
206    // so we check the most efficient alternative first. (`chains` are always in memory,
207    // but `db` stores blocks on disk, with a memory cache.)
208    chains
209        .find_map(|chain| chain.as_ref().ironwood_tree(hash.into()))
210        .or_else(|| db.ironwood_tree_by_hash_or_height(hash.into()))
211}
212
213/// Returns a list of Ironwood [`NoteCommitmentSubtree`]s with indexes in the provided range.
214///
215/// If there is no subtree at the first index in the range, the returned list is empty.
216/// Otherwise, subtrees are continuous up to the finalized tip.
217///
218/// See [`subtrees`] for more details.
219pub fn ironwood_subtrees<C>(
220    chain: Option<C>,
221    db: &ZebraDb,
222    range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
223) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>>
224where
225    C: AsRef<Chain>,
226{
227    subtrees(
228        chain,
229        range,
230        |chain, range| chain.ironwood_subtrees_in_range(range),
231        |range| db.ironwood_subtree_list_by_index_range(range),
232    )
233}
234
235/// Returns a list of [`NoteCommitmentSubtree`]s in the provided range.
236///
237/// If there is no subtree at the first index in the range, the returned list is empty.
238/// Otherwise, subtrees are continuous up to the finalized tip.
239///
240/// Accepts a `chain` from the non-finalized state, a `range` of subtree indexes to retrieve,
241/// a `read_chain` function for retrieving the `range` of subtrees from `chain`, and
242/// a `read_disk` function for retrieving the `range` from [`ZebraDb`].
243///
244/// Returns a consistent set of subtrees for the supplied chain fork and database.
245/// Avoids reading the database if the subtrees are present in memory.
246///
247/// # Correctness
248///
249/// APIs that return single subtrees can't be used for `read_chain` and `read_disk`, because they
250/// can create an inconsistent list of subtrees after concurrent non-finalized and finalized updates.
251fn subtrees<C, Range, Node, ChainSubtreeFn, DbSubtreeFn>(
252    chain: Option<C>,
253    range: Range,
254    read_chain: ChainSubtreeFn,
255    read_disk: DbSubtreeFn,
256) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>
257where
258    C: AsRef<Chain>,
259    Node: PartialEq,
260    Range: std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
261    ChainSubtreeFn: FnOnce(
262        &Chain,
263        Range,
264    )
265        -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
266    DbSubtreeFn:
267        FnOnce(Range) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
268{
269    use std::ops::Bound::*;
270
271    let start_index = match range.start_bound().cloned() {
272        Included(start_index) => start_index,
273        Excluded(start_index) => (start_index.0 + 1).into(),
274        Unbounded => 0.into(),
275    };
276
277    // # Correctness
278    //
279    // After `chain` was cloned, the StateService can commit additional blocks to the finalized state `db`.
280    // Usually, the subtrees of these blocks are consistent. But if the `chain` is a different fork to `db`,
281    // then the trees can be inconsistent. In that case, if `chain` does not contain a subtree at the first
282    // index in the provided range, we ignore all the trees in `chain` after the first inconsistent tree,
283    // because we know they will be inconsistent as well. (It is cryptographically impossible for tree roots
284    // to be equal once the leaves have diverged.)
285
286    let results = match chain.map(|chain| read_chain(chain.as_ref(), range.clone())) {
287        Some(chain_results) if chain_results.contains_key(&start_index) => return chain_results,
288        Some(chain_results) => {
289            let mut db_results = read_disk(range);
290
291            // Check for inconsistent trees in the fork.
292            for (chain_index, chain_subtree) in chain_results {
293                // If there's no matching index, just update the list of trees.
294                let Some(db_subtree) = db_results.get(&chain_index) else {
295                    db_results.insert(chain_index, chain_subtree);
296                    continue;
297                };
298
299                // We have an outdated chain fork, so skip this subtree and all remaining subtrees.
300                if &chain_subtree != db_subtree {
301                    break;
302                }
303                // Otherwise, the subtree is already in the list, so we don't need to add it.
304            }
305
306            db_results
307        }
308        None => read_disk(range),
309    };
310
311    // Check that we got the start subtree
312    if results.contains_key(&start_index) {
313        results
314    } else {
315        BTreeMap::new()
316    }
317}
318
319/// Get the history tree of the provided chain.
320pub fn history_tree<C>(
321    chain: Option<C>,
322    db: &ZebraDb,
323    hash_or_height: HashOrHeight,
324) -> Option<Arc<zebra_chain::history_tree::HistoryTree>>
325where
326    C: AsRef<Chain>,
327{
328    chain
329        .and_then(|chain| chain.as_ref().history_tree(hash_or_height))
330        .or_else(|| Some(db.history_tree()))
331}