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 let mut best_chain = None;
174 let (tx, height, time, in_best_chain, containing_chain) = chains
175 .enumerate()
176 .find_map(|(i, chain)| {
177 chain.as_ref().transaction(hash).map(|(tx, height, time)| {
178 if i == 0 {
179 best_chain = Some(chain);
180 }
181 (tx.clone(), height, time, i == 0, Some(chain))
182 })
183 })
184 .or_else(|| {
185 db.transaction(hash)
186 .map(|(tx, height, time)| (tx.clone(), height, time, true, None))
187 })?;
188
189 if in_best_chain {
190 let confirmations = 1 + tip_height(best_chain, db)?.0 - height.0;
191 Some(AnyTx::Mined(MinedTx::new(tx, height, confirmations, time)))
192 } else {
193 let block_hash = containing_chain?.block(height.into())?.hash;
194 Some(AnyTx::Side((tx, block_hash)))
195 }
196}
197
198/// Returns the [`transaction::Hash`]es for the block with `hash_or_height`,
199/// if it exists in the non-finalized `chain` or finalized `db`.
200///
201/// The returned hashes are in block order.
202///
203/// Returns `None` if the block is not found.
204pub fn transaction_hashes_for_block<C>(
205 chain: Option<C>,
206 db: &ZebraDb,
207 hash_or_height: HashOrHeight,
208) -> Option<Arc<[transaction::Hash]>>
209where
210 C: AsRef<Chain>,
211{
212 // # Correctness
213 //
214 // Since blocks are the same in the finalized and non-finalized state, we
215 // check the most efficient alternative first. (`chain` is always in memory,
216 // but `db` stores blocks on disk, with a memory cache.)
217 chain
218 .as_ref()
219 .and_then(|chain| chain.as_ref().transaction_hashes_for_block(hash_or_height))
220 .or_else(|| db.transaction_hashes_for_block(hash_or_height))
221}
222
223/// Returns the [`transaction::Hash`]es for the block with `hash_or_height`,
224/// if it exists in any chain in `chains` or finalized `db`.
225/// The first chain in `chains` must be the best chain.
226///
227/// The returned hashes are in block order.
228///
229/// Returns `None` if the block is not found.
230pub fn transaction_hashes_for_any_block<'a>(
231 chains: impl Iterator<Item = &'a Arc<Chain>>,
232 db: &ZebraDb,
233 hash_or_height: HashOrHeight,
234) -> Option<(Arc<[transaction::Hash]>, bool)> {
235 // # Correctness
236 //
237 // Since blocks are the same in the finalized and non-finalized state, we
238 // check the most efficient alternative first. (`chain` is always in memory,
239 // but `db` stores blocks on disk, with a memory cache.)
240 chains
241 .enumerate()
242 .find_map(|(i, chain)| {
243 chain
244 .as_ref()
245 .transaction_hashes_for_block(hash_or_height)
246 .map(|hashes| (hashes.clone(), i == 0))
247 })
248 .or_else(|| {
249 db.transaction_hashes_for_block(hash_or_height)
250 .map(|hashes| (hashes, true))
251 })
252}
253
254/// Returns the [`Utxo`] for [`transparent::OutPoint`], if it exists in the
255/// non-finalized `chain` or finalized `db`.
256///
257/// Non-finalized UTXOs are returned regardless of whether they have been spent.
258///
259/// Finalized UTXOs are only returned if they are unspent in the finalized chain.
260/// They may have been spent in the non-finalized chain,
261/// but this function returns them without checking for non-finalized spends,
262/// because we don't know which non-finalized chain will be committed to the finalized state.
263pub fn utxo<C>(chain: Option<C>, db: &ZebraDb, outpoint: transparent::OutPoint) -> Option<Utxo>
264where
265 C: AsRef<Chain>,
266{
267 // # Correctness
268 //
269 // Since UTXOs are the same in the finalized and non-finalized state,
270 // we check the most efficient alternative first. (`chain` is always in
271 // memory, but `db` stores transactions on disk, with a memory cache.)
272 chain
273 .and_then(|chain| chain.as_ref().created_utxo(&outpoint))
274 .or_else(|| db.utxo(&outpoint).map(|utxo| utxo.utxo))
275}
276
277/// Returns the [`Utxo`] for [`transparent::OutPoint`], if it exists and is unspent in the
278/// non-finalized `chain` or finalized `db`.
279pub fn unspent_utxo<C>(
280 chain: Option<C>,
281 db: &ZebraDb,
282 outpoint: transparent::OutPoint,
283) -> Option<Utxo>
284where
285 C: AsRef<Chain>,
286{
287 match chain {
288 Some(chain) if chain.as_ref().spent_utxos.contains_key(&outpoint) => None,
289 chain => utxo(chain, db, outpoint),
290 }
291}
292
293/// Returns the [`Hash`](transaction::Hash) of the transaction that spent an output at
294/// the provided [`transparent::OutPoint`] or revealed the provided nullifier, if it exists
295/// and is spent or revealed in the non-finalized `chain` or finalized `db` and its
296/// spending transaction hash has been indexed.
297#[cfg(feature = "indexer")]
298pub fn spending_transaction_hash<C>(
299 chain: Option<C>,
300 db: &ZebraDb,
301 spend: Spend,
302) -> Option<transaction::Hash>
303where
304 C: AsRef<Chain>,
305{
306 chain
307 .and_then(|chain| chain.as_ref().spending_transaction_hash(&spend))
308 .or_else(|| db.spending_transaction_hash(&spend))
309}
310
311/// Returns the [`Utxo`] for [`transparent::OutPoint`], if it exists in any chain
312/// in the `non_finalized_state`, or in the finalized `db`.
313///
314/// Non-finalized UTXOs are returned regardless of whether they have been spent.
315///
316/// Finalized UTXOs are only returned if they are unspent in the finalized chain.
317/// They may have been spent in one or more non-finalized chains,
318/// but this function returns them without checking for non-finalized spends,
319/// because we don't know which non-finalized chain the request belongs to.
320///
321/// UTXO spends are checked once the block reaches the non-finalized state,
322/// by [`check::utxo::transparent_spend()`](crate::service::check::utxo::transparent_spend).
323pub fn any_utxo(
324 non_finalized_state: NonFinalizedState,
325 db: &ZebraDb,
326 outpoint: transparent::OutPoint,
327) -> Option<Utxo> {
328 // # Correctness
329 //
330 // Since UTXOs are the same in the finalized and non-finalized state,
331 // we check the most efficient alternative first. (`non_finalized_state` is always in
332 // memory, but `db` stores transactions on disk, with a memory cache.)
333 non_finalized_state
334 .any_utxo(&outpoint)
335 .or_else(|| db.utxo(&outpoint).map(|utxo| utxo.utxo))
336}
337
338/// Returns the [`BlockInfo`] with [`block::Hash`] or
339/// [`Height`], if it exists in the non-finalized `chain` or finalized `db`.
340pub fn block_info<C>(
341 chain: Option<C>,
342 db: &ZebraDb,
343 hash_or_height: HashOrHeight,
344) -> Option<BlockInfo>
345where
346 C: AsRef<Chain>,
347{
348 // # Correctness
349 //
350 // Since blocks are the same in the finalized and non-finalized state, we
351 // check the most efficient alternative first. (`chain` is always in memory,
352 // but `db` stores blocks on disk, with a memory cache.)
353 chain
354 .as_ref()
355 .and_then(|chain| chain.as_ref().block_info(hash_or_height))
356 .or_else(|| db.block_info(hash_or_height))
357}