zebra_state/service/read/address/
utxo.rs

1//! Transparent address index UTXO queries.
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::{
15    collections::{BTreeMap, BTreeSet, HashSet},
16    ops::RangeInclusive,
17};
18
19use zebra_chain::{block::Height, parameters::Network, transaction, transparent};
20
21use crate::{
22    service::{
23        finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES,
24    },
25    BoxError, OutputLocation, TransactionLocation,
26};
27
28/// The full range of address heights.
29///
30/// The genesis coinbase transactions are ignored by a consensus rule,
31/// so they are not included in any address indexes.
32pub const ADDRESS_HEIGHTS_FULL_RANGE: RangeInclusive<Height> = Height(1)..=Height::MAX;
33
34/// A convenience wrapper that efficiently stores unspent transparent outputs,
35/// and the corresponding transaction IDs.
36#[derive(Clone, Debug, Default, Eq, PartialEq)]
37pub struct AddressUtxos {
38    /// A set of unspent transparent outputs.
39    utxos: BTreeMap<OutputLocation, transparent::Output>,
40
41    /// The transaction IDs for each [`OutputLocation`] in `utxos`.
42    tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
43
44    /// The configured network for this state.
45    network: Network,
46}
47
48impl AddressUtxos {
49    /// Creates a new set of address UTXOs.
50    pub fn new(
51        network: &Network,
52        utxos: BTreeMap<OutputLocation, transparent::Output>,
53        tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
54    ) -> Self {
55        Self {
56            utxos,
57            tx_ids,
58            network: network.clone(),
59        }
60    }
61
62    /// Returns an iterator that provides the unspent output, its transaction hash,
63    /// its location in the chain, and the address it was sent to.
64    ///
65    /// The UTXOs are returned in chain order, across all addresses.
66    #[allow(dead_code)]
67    pub fn utxos(
68        &self,
69    ) -> impl Iterator<
70        Item = (
71            transparent::Address,
72            &transaction::Hash,
73            &OutputLocation,
74            &transparent::Output,
75        ),
76    > {
77        self.utxos.iter().map(|(out_loc, output)| {
78            (
79                output
80                    .address(&self.network)
81                    .expect("address indexes only contain outputs with addresses"),
82                self.tx_ids
83                    .get(&out_loc.transaction_location())
84                    .expect("address indexes are consistent"),
85                out_loc,
86                output,
87            )
88        })
89    }
90}
91
92/// Returns the unspent transparent outputs (UTXOs) for the supplied [`transparent::Address`]es,
93/// in chain order; and the transaction IDs for the transactions containing those UTXOs.
94///
95/// If the addresses do not exist in the non-finalized `chain` or finalized `db`,
96/// returns an empty list.
97pub fn address_utxos<C>(
98    network: &Network,
99    chain: Option<C>,
100    db: &ZebraDb,
101    addresses: HashSet<transparent::Address>,
102) -> Result<AddressUtxos, BoxError>
103where
104    C: AsRef<Chain>,
105{
106    let mut utxo_error = None;
107    let address_count = addresses.len();
108
109    // Retry the finalized UTXO query if it was interrupted by a finalizing block,
110    // and the non-finalized chain doesn't overlap the changed heights.
111    //
112    // TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn
113    for attempt in 0..=FINALIZED_STATE_QUERY_RETRIES {
114        debug!(?attempt, ?address_count, "starting address UTXO query");
115
116        let (finalized_utxos, finalized_tip_range) = finalized_address_utxos(db, &addresses);
117
118        debug!(
119            finalized_utxo_count = ?finalized_utxos.len(),
120            ?finalized_tip_range,
121            ?address_count,
122            ?attempt,
123            "finalized address UTXO response",
124        );
125
126        // Apply the non-finalized UTXO changes.
127        let chain_utxo_changes =
128            chain_transparent_utxo_changes(chain.as_ref(), &addresses, finalized_tip_range);
129
130        // If the UTXOs are valid, return them, otherwise, retry or return an error.
131        match chain_utxo_changes {
132            Ok((created_chain_utxos, spent_chain_utxos)) => {
133                debug!(
134                    chain_utxo_count = ?created_chain_utxos.len(),
135                    chain_utxo_spent = ?spent_chain_utxos.len(),
136                    ?address_count,
137                    ?attempt,
138                    "chain address UTXO response",
139                );
140
141                let utxos =
142                    apply_utxo_changes(finalized_utxos, created_chain_utxos, spent_chain_utxos);
143                let tx_ids = lookup_tx_ids_for_utxos(chain, db, &addresses, &utxos);
144
145                debug!(
146                    full_utxo_count = ?utxos.len(),
147                    tx_id_count = ?tx_ids.len(),
148                    ?address_count,
149                    ?attempt,
150                    "full address UTXO response",
151                );
152
153                return Ok(AddressUtxos::new(network, utxos, tx_ids));
154            }
155
156            Err(chain_utxo_error) => {
157                debug!(
158                    ?chain_utxo_error,
159                    ?address_count,
160                    ?attempt,
161                    "chain address UTXO response",
162                );
163
164                utxo_error = Some(Err(chain_utxo_error))
165            }
166        }
167    }
168
169    utxo_error.expect("unexpected missing error: attempts should set error or return")
170}
171
172/// Returns the unspent transparent outputs (UTXOs) for `addresses` in the finalized chain,
173/// and the finalized tip heights the UTXOs were queried at.
174///
175/// If the addresses do not exist in the finalized `db`, returns an empty list.
176//
177// TODO: turn the return type into a struct?
178fn finalized_address_utxos(
179    db: &ZebraDb,
180    addresses: &HashSet<transparent::Address>,
181) -> (
182    BTreeMap<OutputLocation, transparent::Output>,
183    Option<RangeInclusive<Height>>,
184) {
185    // # Correctness
186    //
187    // The StateService can commit additional blocks while we are querying address UTXOs.
188
189    // Check if the finalized state changed while we were querying it
190    let start_finalized_tip = db.finalized_tip_height();
191
192    let finalized_utxos = db.partial_finalized_address_utxos(addresses);
193
194    let end_finalized_tip = db.finalized_tip_height();
195
196    let finalized_tip_range = if let (Some(start_finalized_tip), Some(end_finalized_tip)) =
197        (start_finalized_tip, end_finalized_tip)
198    {
199        Some(start_finalized_tip..=end_finalized_tip)
200    } else {
201        // State is empty
202        None
203    };
204
205    (finalized_utxos, finalized_tip_range)
206}
207
208/// Returns the UTXO changes for `addresses` in the non-finalized chain,
209/// matching or overlapping the UTXOs for the `finalized_tip_range`.
210///
211/// If the addresses do not exist in the non-finalized `chain`, returns an empty list.
212//
213// TODO: turn the return type into a struct?
214fn chain_transparent_utxo_changes<C>(
215    chain: Option<C>,
216    addresses: &HashSet<transparent::Address>,
217    finalized_tip_range: Option<RangeInclusive<Height>>,
218) -> Result<
219    (
220        BTreeMap<OutputLocation, transparent::Output>,
221        BTreeSet<OutputLocation>,
222    ),
223    BoxError,
224>
225where
226    C: AsRef<Chain>,
227{
228    let address_count = addresses.len();
229
230    let finalized_tip_range = match finalized_tip_range {
231        Some(finalized_tip_range) => finalized_tip_range,
232        None => {
233            assert!(
234                chain.is_none(),
235                "unexpected non-finalized chain when finalized state is empty"
236            );
237
238            debug!(
239                ?finalized_tip_range,
240                ?address_count,
241                "chain address UTXO query: state is empty, no UTXOs available",
242            );
243
244            return Ok(Default::default());
245        }
246    };
247
248    // # Correctness
249    //
250    // We can compensate for deleted UTXOs by applying the overlapping non-finalized UTXO changes.
251
252    // Check if the finalized and non-finalized states match or overlap
253    let required_min_non_finalized_root = finalized_tip_range.start().0 + 1;
254
255    // Work out if we need to compensate for finalized query results from multiple heights:
256    // - Ok contains the finalized tip height (no need to compensate)
257    // - Err contains the required non-finalized chain overlap
258    let finalized_tip_status = required_min_non_finalized_root..=finalized_tip_range.end().0;
259    let finalized_tip_status = if finalized_tip_status.is_empty() {
260        let finalized_tip_height = *finalized_tip_range.end();
261        Ok(finalized_tip_height)
262    } else {
263        let required_non_finalized_overlap = finalized_tip_status;
264        Err(required_non_finalized_overlap)
265    };
266
267    if chain.is_none() {
268        if finalized_tip_status.is_ok() {
269            debug!(
270                ?finalized_tip_status,
271                ?required_min_non_finalized_root,
272                ?finalized_tip_range,
273                ?address_count,
274                "chain address UTXO query: \
275                 finalized chain is consistent, and non-finalized chain is empty",
276            );
277
278            return Ok(Default::default());
279        } else {
280            // We can't compensate for inconsistent database queries,
281            // because the non-finalized chain is empty.
282            debug!(
283                ?finalized_tip_status,
284                ?required_min_non_finalized_root,
285                ?finalized_tip_range,
286                ?address_count,
287                "chain address UTXO query: \
288                 finalized tip query was inconsistent, but non-finalized chain is empty",
289            );
290
291            return Err("unable to get UTXOs: \
292                        state was committing a block, and non-finalized chain is empty"
293                .into());
294        }
295    }
296
297    let chain = chain.unwrap();
298    let chain = chain.as_ref();
299
300    let non_finalized_root = chain.non_finalized_root_height();
301    let non_finalized_tip = chain.non_finalized_tip_height();
302
303    assert!(
304        non_finalized_root.0 <= required_min_non_finalized_root,
305        "unexpected chain gap: the best chain is updated after its previous root is finalized",
306    );
307
308    match finalized_tip_status {
309        Ok(finalized_tip_height) => {
310            // If we've already committed this entire chain, ignore its UTXO changes.
311            // This is more likely if the non-finalized state is just getting started.
312            if finalized_tip_height >= non_finalized_tip {
313                debug!(
314                    ?non_finalized_root,
315                    ?non_finalized_tip,
316                    ?finalized_tip_status,
317                    ?finalized_tip_range,
318                    ?address_count,
319                    "chain address UTXO query: \
320                     non-finalized blocks have all been finalized, no new UTXO changes",
321                );
322
323                return Ok(Default::default());
324            }
325        }
326
327        Err(ref required_non_finalized_overlap) => {
328            // We can't compensate for inconsistent database queries,
329            // because the non-finalized chain is below the inconsistent query range.
330            if *required_non_finalized_overlap.end() > non_finalized_tip.0 {
331                debug!(
332                    ?non_finalized_root,
333                    ?non_finalized_tip,
334                    ?finalized_tip_status,
335                    ?finalized_tip_range,
336                    ?address_count,
337                    "chain address UTXO query: \
338                     finalized tip query was inconsistent, \
339                     and some inconsistent blocks are missing from the non-finalized chain",
340                );
341
342                return Err("unable to get UTXOs: \
343                            state was committing a block, \
344                            that is missing from the non-finalized chain"
345                    .into());
346            }
347
348            // Correctness: some finalized UTXOs might have duplicate creates or spends,
349            // but we've just checked they can be corrected by applying the non-finalized UTXO changes.
350            assert!(
351                required_non_finalized_overlap
352                    .clone()
353                    .all(|height| chain.blocks.contains_key(&Height(height))),
354                "UTXO query inconsistency: chain must contain required overlap blocks",
355            );
356        }
357    }
358
359    Ok(chain.partial_transparent_utxo_changes(addresses))
360}
361
362/// Combines the supplied finalized and non-finalized UTXOs,
363/// removes the spent UTXOs, and returns the result.
364fn apply_utxo_changes(
365    finalized_utxos: BTreeMap<OutputLocation, transparent::Output>,
366    created_chain_utxos: BTreeMap<OutputLocation, transparent::Output>,
367    spent_chain_utxos: BTreeSet<OutputLocation>,
368) -> BTreeMap<OutputLocation, transparent::Output> {
369    // Correctness: combine the created UTXOs, then remove spent UTXOs,
370    // to compensate for overlapping finalized and non-finalized blocks.
371    finalized_utxos
372        .into_iter()
373        .chain(created_chain_utxos)
374        .filter(|(utxo_location, _output)| !spent_chain_utxos.contains(utxo_location))
375        .collect()
376}
377
378/// Returns the [`transaction::Hash`]es containing the supplied UTXOs,
379/// from the non-finalized `chain` and finalized `db`.
380///
381/// # Panics
382///
383/// If any UTXO is not in the supplied state.
384fn lookup_tx_ids_for_utxos<C>(
385    chain: Option<C>,
386    db: &ZebraDb,
387    addresses: &HashSet<transparent::Address>,
388    utxos: &BTreeMap<OutputLocation, transparent::Output>,
389) -> BTreeMap<TransactionLocation, transaction::Hash>
390where
391    C: AsRef<Chain>,
392{
393    // Get the unique set of transaction locations
394    let transaction_locations: BTreeSet<TransactionLocation> = utxos
395        .keys()
396        .map(|output_location| output_location.transaction_location())
397        .collect();
398
399    let chain_tx_ids = chain
400        .as_ref()
401        .map(|chain| {
402            chain
403                .as_ref()
404                .partial_transparent_tx_ids(addresses, ADDRESS_HEIGHTS_FULL_RANGE)
405        })
406        .unwrap_or_default();
407
408    // First try the in-memory chain, then the disk database
409    transaction_locations
410        .iter()
411        .map(|tx_loc| {
412            (
413                *tx_loc,
414                chain_tx_ids.get(tx_loc).cloned().unwrap_or_else(|| {
415                    db.transaction_hash(*tx_loc)
416                        .expect("unexpected inconsistent UTXO indexes")
417                }),
418            )
419        })
420        .collect()
421}