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}