Skip to main content

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}