zebra_state/service/read/block.rs
1//! Shared block, header, and transaction reading code.
2//!
3//! In the functions in this module:
4//!
5//! The block write task commits blocks to the finalized state before updating
6//! `chain` or `non_finalized_state` with a cached copy of the non-finalized chains
7//! in `NonFinalizedState.chain_set`. Then the block commit task can
8//! commit additional blocks to the finalized state after we've cloned the
9//! `chain` or `non_finalized_state`.
10//!
11//! This means that some blocks can be in both:
12//! - the cached [`Chain`] or [`NonFinalizedState`], and
13//! - the shared finalized [`ZebraDb`] reference.
14
15use std::sync::Arc;
16
17use chrono::{DateTime, Utc};
18
19use zebra_chain::{
20 block::{self, Block, Height},
21 block_info::BlockInfo,
22 serialization::ZcashSerialize as _,
23 transaction::{self, Transaction},
24 transparent::{self, Utxo},
25};
26
27use crate::{
28 response::{AnyTx, MinedTx},
29 service::{
30 finalized_state::ZebraDb,
31 non_finalized_state::{Chain, NonFinalizedState},
32 read::tip_height,
33 },
34 HashOrHeight,
35};
36
37#[cfg(feature = "indexer")]
38use crate::request::Spend;
39
40/// Returns the [`Block`] with [`block::Hash`] or
41/// [`Height`], if it exists in the non-finalized `chains` or finalized `db`.
42pub fn any_block<'a, C: AsRef<Chain> + 'a>(
43 mut chains: impl Iterator<Item = &'a C>,
44 db: &ZebraDb,
45 hash_or_height: HashOrHeight,
46) -> Option<Arc<Block>> {
47 // # Correctness
48 //
49 // Since blocks are the same in the finalized and non-finalized state, we
50 // check the most efficient alternative first. (`chain` is always in memory,
51 // but `db` stores blocks on disk, with a memory cache.)
52 chains
53 .find_map(|c| c.as_ref().block(hash_or_height))
54 .map(|contextual| contextual.block.clone())
55 .or_else(|| db.block(hash_or_height))
56}
57
58/// Returns the [`Block`] with [`block::Hash`] or
59/// [`Height`], if it exists in the non-finalized `chain` or finalized `db`.
60pub fn block<C>(chain: Option<C>, db: &ZebraDb, hash_or_height: HashOrHeight) -> Option<Arc<Block>>
61where
62 C: AsRef<Chain>,
63{
64 any_block(chain.iter(), db, hash_or_height)
65}
66
67/// Returns the [`Block`] with [`block::Hash`] or
68/// [`Height`], if it exists in the non-finalized `chain` or finalized `db`.
69pub fn block_and_size<C>(
70 chain: Option<C>,
71 db: &ZebraDb,
72 hash_or_height: HashOrHeight,
73) -> Option<(Arc<Block>, usize)>
74where
75 C: AsRef<Chain>,
76{
77 // # Correctness
78 //
79 // Since blocks are the same in the finalized and non-finalized state, we
80 // check the most efficient alternative first. (`chain` is always in memory,
81 // but `db` stores blocks on disk, with a memory cache.)
82 chain
83 .as_ref()
84 .and_then(|chain| chain.as_ref().block(hash_or_height))
85 .map(|contextual| {
86 let size = contextual.block.zcash_serialize_to_vec().unwrap().len();
87 (contextual.block.clone(), size)
88 })
89 .or_else(|| db.block_and_size(hash_or_height))
90}
91
92/// Returns the [`block::Header`] with [`block::Hash`] or
93/// [`Height`], if it exists in the non-finalized `chain` or finalized `db`.
94pub fn block_header<C>(
95 chain: Option<C>,
96 db: &ZebraDb,
97 hash_or_height: HashOrHeight,
98) -> Option<Arc<block::Header>>
99where
100 C: AsRef<Chain>,
101{
102 // # Correctness
103 //
104 // Since blocks are the same in the finalized and non-finalized state, we
105 // check the most efficient alternative first. (`chain` is always in memory,
106 // but `db` stores blocks on disk, with a memory cache.)
107 chain
108 .as_ref()
109 .and_then(|chain| chain.as_ref().block(hash_or_height))
110 .map(|contextual| contextual.block.header.clone())
111 .or_else(|| db.block_header(hash_or_height))
112}
113
114/// Returns the [`Transaction`] with [`transaction::Hash`], if it exists in the
115/// non-finalized `chain` or finalized `db`.
116fn transaction<C>(
117 chain: Option<C>,
118 db: &ZebraDb,
119 hash: transaction::Hash,
120) -> Option<(Arc<Transaction>, Height, DateTime<Utc>)>
121where
122 C: AsRef<Chain>,
123{
124 // # Correctness
125 //
126 // Since transactions are the same in the finalized and non-finalized state,
127 // we check the most efficient alternative first. (`chain` is always in
128 // memory, but `db` stores transactions on disk, with a memory cache.)
129 chain
130 .and_then(|chain| {
131 chain
132 .as_ref()
133 .transaction(hash)
134 .map(|(tx, height, time)| (tx.clone(), height, time))
135 })
136 .or_else(|| db.transaction(hash))
137}
138
139/// Returns a [`MinedTx`] for a [`Transaction`] with [`transaction::Hash`],
140/// if one exists in the non-finalized `chain` or finalized `db`.
141pub fn mined_transaction<C>(
142 chain: Option<C>,
143 db: &ZebraDb,
144 hash: transaction::Hash,
145) -> Option<MinedTx>
146where
147 C: AsRef<Chain>,
148{
149 // # Correctness
150 //
151 // It is ok to do this lookup in two different calls. Finalized state updates
152 // can only add overlapping blocks, and hashes are unique.
153 let chain = chain.as_ref();
154
155 let (tx, height, time) = transaction(chain, db, hash)?;
156 let confirmations = 1 + tip_height(chain, db)?.0 - height.0;
157
158 Some(MinedTx::new(tx, height, confirmations, time))
159}
160
161/// Returns a [`AnyTx`] for a [`Transaction`] with [`transaction::Hash`],
162/// if one exists in any chain in `chains` or finalized `db`.
163/// The first chain in `chains` must be the best chain.
164pub fn any_transaction<'a>(
165 chains: impl Iterator<Item = &'a Arc<Chain>>,
166 db: &ZebraDb,
167 hash: transaction::Hash,
168) -> Option<AnyTx> {
169 // # Correctness
170 //
171 // It is ok to do this lookup in multiple different calls. Finalized state updates
172 // can only add overlapping blocks, and hashes are unique.
173 //
174 // Capture the best chain tip before searching, not inside the search closure.
175 // The closure only runs when the tx is found in a non-finalized chain; if the tx
176 // is only in the finalized DB, the closure never fires and best_chain would stay
177 // None, causing tip_height to undercount confirmations by ~MAX_BLOCK_REORG_HEIGHT.
178 // See <https://github.com/ZcashFoundation/zebra/issues/10470>.
179 // peekable() reads the first element without consuming it, so the iterator can
180 // still be used in find_map below.
181 let mut chains = chains.peekable();
182 let best_chain = chains.peek().copied();
183 let (tx, height, time, in_best_chain, containing_chain) = chains
184 .enumerate()
185 .find_map(|(i, chain)| {
186 chain
187 .as_ref()
188 .transaction(hash)
189 .map(|(tx, height, time)| (tx.clone(), height, time, i == 0, Some(chain)))
190 })
191 .or_else(|| {
192 db.transaction(hash)
193 .map(|(tx, height, time)| (tx.clone(), height, time, true, None))
194 })?;
195
196 if in_best_chain {
197 let confirmations = 1 + tip_height(best_chain, db)?.0 - height.0;
198 Some(AnyTx::Mined(MinedTx::new(tx, height, confirmations, time)))
199 } else {
200 let block_hash = containing_chain?.block(height.into())?.hash;
201 Some(AnyTx::Side((tx, block_hash)))
202 }
203}
204
205/// Returns the [`transaction::Hash`]es for the block with `hash_or_height`,
206/// if it exists in the non-finalized `chain` or finalized `db`.
207///
208/// The returned hashes are in block order.
209///
210/// Returns `None` if the block is not found.
211pub fn transaction_hashes_for_block<C>(
212 chain: Option<C>,
213 db: &ZebraDb,
214 hash_or_height: HashOrHeight,
215) -> Option<Arc<[transaction::Hash]>>
216where
217 C: AsRef<Chain>,
218{
219 // # Correctness
220 //
221 // Since blocks are the same in the finalized and non-finalized state, we
222 // check the most efficient alternative first. (`chain` is always in memory,
223 // but `db` stores blocks on disk, with a memory cache.)
224 chain
225 .as_ref()
226 .and_then(|chain| chain.as_ref().transaction_hashes_for_block(hash_or_height))
227 .or_else(|| db.transaction_hashes_for_block(hash_or_height))
228}
229
230/// Returns the [`transaction::Hash`]es for the block with `hash_or_height`,
231/// if it exists in any chain in `chains` or finalized `db`.
232/// The first chain in `chains` must be the best chain.
233///
234/// The returned hashes are in block order.
235///
236/// Returns `None` if the block is not found.
237pub fn transaction_hashes_for_any_block<'a>(
238 chains: impl Iterator<Item = &'a Arc<Chain>>,
239 db: &ZebraDb,
240 hash_or_height: HashOrHeight,
241) -> Option<(Arc<[transaction::Hash]>, bool)> {
242 // # Correctness
243 //
244 // Since blocks are the same in the finalized and non-finalized state, we
245 // check the most efficient alternative first. (`chain` is always in memory,
246 // but `db` stores blocks on disk, with a memory cache.)
247 chains
248 .enumerate()
249 .find_map(|(i, chain)| {
250 chain
251 .as_ref()
252 .transaction_hashes_for_block(hash_or_height)
253 .map(|hashes| (hashes.clone(), i == 0))
254 })
255 .or_else(|| {
256 db.transaction_hashes_for_block(hash_or_height)
257 .map(|hashes| (hashes, true))
258 })
259}
260
261/// Returns the [`Utxo`] for [`transparent::OutPoint`], if it exists in the
262/// non-finalized `chain` or finalized `db`.
263///
264/// Non-finalized UTXOs are returned regardless of whether they have been spent.
265///
266/// Finalized UTXOs are only returned if they are unspent in the finalized chain.
267/// They may have been spent in the non-finalized chain,
268/// but this function returns them without checking for non-finalized spends,
269/// because we don't know which non-finalized chain will be committed to the finalized state.
270pub fn utxo<C>(chain: Option<C>, db: &ZebraDb, outpoint: transparent::OutPoint) -> Option<Utxo>
271where
272 C: AsRef<Chain>,
273{
274 // # Correctness
275 //
276 // Since UTXOs are the same in the finalized and non-finalized state,
277 // we check the most efficient alternative first. (`chain` is always in
278 // memory, but `db` stores transactions on disk, with a memory cache.)
279 chain
280 .and_then(|chain| chain.as_ref().created_utxo(&outpoint))
281 .or_else(|| db.utxo(&outpoint).map(|utxo| utxo.utxo))
282}
283
284/// Returns the [`Utxo`] for [`transparent::OutPoint`], if it exists and is unspent in the
285/// non-finalized `chain` or finalized `db`.
286pub fn unspent_utxo<C>(
287 chain: Option<C>,
288 db: &ZebraDb,
289 outpoint: transparent::OutPoint,
290) -> Option<Utxo>
291where
292 C: AsRef<Chain>,
293{
294 match chain {
295 Some(chain) if chain.as_ref().spent_utxos.contains_key(&outpoint) => None,
296 chain => utxo(chain, db, outpoint),
297 }
298}
299
300/// Returns the [`Hash`](transaction::Hash) of the transaction that spent an output at
301/// the provided [`transparent::OutPoint`] or revealed the provided nullifier, if it exists
302/// and is spent or revealed in the non-finalized `chain` or finalized `db` and its
303/// spending transaction hash has been indexed.
304#[cfg(feature = "indexer")]
305pub fn spending_transaction_hash<C>(
306 chain: Option<C>,
307 db: &ZebraDb,
308 spend: Spend,
309) -> Option<transaction::Hash>
310where
311 C: AsRef<Chain>,
312{
313 chain
314 .and_then(|chain| chain.as_ref().spending_transaction_hash(&spend))
315 .or_else(|| db.spending_transaction_hash(&spend))
316}
317
318/// Returns the [`Utxo`] for [`transparent::OutPoint`], if it exists in any chain
319/// in the `non_finalized_state`, or in the finalized `db`.
320///
321/// Non-finalized UTXOs are returned regardless of whether they have been spent.
322///
323/// Finalized UTXOs are only returned if they are unspent in the finalized chain.
324/// They may have been spent in one or more non-finalized chains,
325/// but this function returns them without checking for non-finalized spends,
326/// because we don't know which non-finalized chain the request belongs to.
327///
328/// UTXO spends are checked once the block reaches the non-finalized state,
329/// by [`check::utxo::transparent_spend()`](crate::service::check::utxo::transparent_spend).
330pub fn any_utxo(
331 non_finalized_state: NonFinalizedState,
332 db: &ZebraDb,
333 outpoint: transparent::OutPoint,
334) -> Option<Utxo> {
335 // # Correctness
336 //
337 // Since UTXOs are the same in the finalized and non-finalized state,
338 // we check the most efficient alternative first. (`non_finalized_state` is always in
339 // memory, but `db` stores transactions on disk, with a memory cache.)
340 non_finalized_state
341 .any_utxo(&outpoint)
342 .or_else(|| db.utxo(&outpoint).map(|utxo| utxo.utxo))
343}
344
345/// Returns the [`BlockInfo`] with [`block::Hash`] or
346/// [`Height`], if it exists in the non-finalized `chain` or finalized `db`.
347pub fn block_info<C>(
348 chain: Option<C>,
349 db: &ZebraDb,
350 hash_or_height: HashOrHeight,
351) -> Option<BlockInfo>
352where
353 C: AsRef<Chain>,
354{
355 // # Correctness
356 //
357 // Since blocks are the same in the finalized and non-finalized state, we
358 // check the most efficient alternative first. (`chain` is always in memory,
359 // but `db` stores blocks on disk, with a memory cache.)
360 chain
361 .as_ref()
362 .and_then(|chain| chain.as_ref().block_info(hash_or_height))
363 .or_else(|| db.block_info(hash_or_height))
364}