zebra_state/service/read/address/
tx_id.rs

1//! Reading address transaction IDs.
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, HashSet},
16    ops::RangeInclusive,
17};
18
19use zebra_chain::{block::Height, transaction, transparent};
20
21use crate::{
22    service::{
23        finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES,
24    },
25    BoxError, TransactionLocation,
26};
27
28/// Returns the transaction IDs that sent or received funds from the supplied [`transparent::Address`]es,
29/// within `query_height_range`, in chain order.
30///
31/// If the addresses do not exist in the non-finalized `chain` or finalized `db`,
32/// or the `query_height_range` is totally outside both the `chain` and `db` range,
33/// returns an empty list.
34pub fn transparent_tx_ids<C>(
35    chain: Option<C>,
36    db: &ZebraDb,
37    addresses: HashSet<transparent::Address>,
38    query_height_range: RangeInclusive<Height>,
39) -> Result<BTreeMap<TransactionLocation, transaction::Hash>, BoxError>
40where
41    C: AsRef<Chain>,
42{
43    let mut tx_id_error = None;
44
45    // Retry the finalized tx ID query if it was interrupted by a finalizing block,
46    // and the non-finalized chain doesn't overlap the changed heights.
47    //
48    // TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn
49    for _ in 0..=FINALIZED_STATE_QUERY_RETRIES {
50        let (finalized_tx_ids, finalized_tip_range) =
51            finalized_transparent_tx_ids(db, &addresses, query_height_range.clone());
52
53        // Apply the non-finalized tx ID changes.
54        let chain_tx_id_changes = chain_transparent_tx_id_changes(
55            chain.as_ref(),
56            &addresses,
57            finalized_tip_range,
58            query_height_range.clone(),
59        );
60
61        // If the tx IDs are valid, return them, otherwise, retry or return an error.
62        match chain_tx_id_changes {
63            Ok(chain_tx_id_changes) => {
64                let tx_ids = apply_tx_id_changes(finalized_tx_ids, chain_tx_id_changes);
65
66                return Ok(tx_ids);
67            }
68
69            Err(error) => tx_id_error = Some(Err(error)),
70        }
71    }
72
73    tx_id_error.expect("unexpected missing error: attempts should set error or return")
74}
75
76/// Returns the [`transaction::Hash`]es for `addresses` in the finalized chain `query_height_range`,
77/// and the finalized tip heights the transaction IDs were queried at.
78///
79/// If the addresses do not exist in the finalized `db`, returns an empty list.
80//
81// TODO: turn the return type into a struct?
82fn finalized_transparent_tx_ids(
83    db: &ZebraDb,
84    addresses: &HashSet<transparent::Address>,
85    query_height_range: RangeInclusive<Height>,
86) -> (
87    BTreeMap<TransactionLocation, transaction::Hash>,
88    Option<RangeInclusive<Height>>,
89) {
90    // # Correctness
91    //
92    // The StateService can commit additional blocks while we are querying transaction IDs.
93
94    // Check if the finalized state changed while we were querying it
95    let start_finalized_tip = db.finalized_tip_height();
96
97    let finalized_tx_ids = db.partial_finalized_transparent_tx_ids(addresses, query_height_range);
98
99    let end_finalized_tip = db.finalized_tip_height();
100
101    let finalized_tip_range = if let (Some(start_finalized_tip), Some(end_finalized_tip)) =
102        (start_finalized_tip, end_finalized_tip)
103    {
104        Some(start_finalized_tip..=end_finalized_tip)
105    } else {
106        // State is empty
107        None
108    };
109
110    (finalized_tx_ids, finalized_tip_range)
111}
112
113/// Returns the extra transaction IDs for `addresses` in the non-finalized chain `query_height_range`,
114/// matching or overlapping the transaction IDs for the `finalized_tip_range`,
115///
116/// If the addresses do not exist in the non-finalized `chain`, returns an empty list.
117//
118// TODO: turn the return type into a struct?
119fn chain_transparent_tx_id_changes<C>(
120    chain: Option<C>,
121    addresses: &HashSet<transparent::Address>,
122    finalized_tip_range: Option<RangeInclusive<Height>>,
123    query_height_range: RangeInclusive<Height>,
124) -> Result<BTreeMap<TransactionLocation, transaction::Hash>, BoxError>
125where
126    C: AsRef<Chain>,
127{
128    let address_count = addresses.len();
129
130    let finalized_tip_range = match finalized_tip_range {
131        Some(finalized_tip_range) => finalized_tip_range,
132        None => {
133            assert!(
134                chain.is_none(),
135                "unexpected non-finalized chain when finalized state is empty"
136            );
137
138            debug!(
139                ?finalized_tip_range,
140                ?address_count,
141                "chain address tx ID query: state is empty, no tx IDs available",
142            );
143
144            return Ok(Default::default());
145        }
146    };
147
148    // # Correctness
149    //
150    // We can compensate for addresses with mismatching blocks,
151    // by adding the overlapping non-finalized transaction IDs.
152    //
153    // If there is only one address, mismatches aren't possible,
154    // because tx IDs are added to the finalized state in chain order (and never removed),
155    // and they are queried in chain order.
156
157    // Check if the finalized and non-finalized states match or overlap
158    let required_min_non_finalized_root = finalized_tip_range.start().0 + 1;
159
160    // Work out if we need to compensate for finalized query results from multiple heights:
161    // - Ok contains the finalized tip height (no need to compensate)
162    // - Err contains the required non-finalized chain overlap
163    let finalized_tip_status = required_min_non_finalized_root..=finalized_tip_range.end().0;
164    let finalized_tip_status = if finalized_tip_status.is_empty() {
165        let finalized_tip_height = *finalized_tip_range.end();
166        Ok(finalized_tip_height)
167    } else {
168        let required_non_finalized_overlap = finalized_tip_status;
169        Err(required_non_finalized_overlap)
170    };
171
172    if chain.is_none() {
173        if address_count <= 1 || finalized_tip_status.is_ok() {
174            debug!(
175                ?finalized_tip_status,
176                ?required_min_non_finalized_root,
177                ?finalized_tip_range,
178                ?address_count,
179                "chain address tx ID query: \
180                 finalized chain is consistent, and non-finalized chain is empty",
181            );
182
183            return Ok(Default::default());
184        } else {
185            // We can't compensate for inconsistent database queries,
186            // because the non-finalized chain is empty.
187            debug!(
188                ?finalized_tip_status,
189                ?required_min_non_finalized_root,
190                ?finalized_tip_range,
191                ?address_count,
192                "chain address tx ID query: \
193                 finalized tip query was inconsistent, but non-finalized chain is empty",
194            );
195
196            return Err("unable to get tx IDs: \
197                        state was committing a block, and non-finalized chain is empty"
198                .into());
199        }
200    }
201
202    let chain = chain.unwrap();
203    let chain = chain.as_ref();
204
205    let non_finalized_root = chain.non_finalized_root_height();
206    let non_finalized_tip = chain.non_finalized_tip_height();
207
208    assert!(
209        non_finalized_root.0 <= required_min_non_finalized_root,
210        "unexpected chain gap: the best chain is updated after its previous root is finalized",
211    );
212
213    match finalized_tip_status {
214        Ok(finalized_tip_height) => {
215            // If we've already committed this entire chain, ignore its UTXO changes.
216            // This is more likely if the non-finalized state is just getting started.
217            if finalized_tip_height >= non_finalized_tip {
218                debug!(
219                    ?non_finalized_root,
220                    ?non_finalized_tip,
221                    ?finalized_tip_status,
222                    ?finalized_tip_range,
223                    ?address_count,
224                    "chain address tx ID query: \
225                     non-finalized blocks have all been finalized, no new UTXO changes",
226                );
227
228                return Ok(Default::default());
229            }
230        }
231
232        Err(ref required_non_finalized_overlap) => {
233            // We can't compensate for inconsistent database queries,
234            // because the non-finalized chain is below the inconsistent query range.
235            if address_count > 1 && *required_non_finalized_overlap.end() > non_finalized_tip.0 {
236                debug!(
237                    ?non_finalized_root,
238                    ?non_finalized_tip,
239                    ?finalized_tip_status,
240                    ?finalized_tip_range,
241                    ?address_count,
242                    "chain address tx ID query: \
243                     finalized tip query was inconsistent, \
244                     some inconsistent blocks are missing from the non-finalized chain, \
245                     and the query has multiple addresses",
246                );
247
248                return Err("unable to get tx IDs: \
249                            state was committing a block, \
250                            that is missing from the non-finalized chain, \
251                            and the query has multiple addresses"
252                    .into());
253            }
254
255            // Correctness: some finalized UTXOs might have duplicate creates or spends,
256            // but we've just checked they can be corrected by applying the non-finalized UTXO changes.
257            assert!(
258                address_count <= 1
259                    || required_non_finalized_overlap
260                        .clone()
261                        .all(|height| chain.blocks.contains_key(&Height(height))),
262                "tx ID query inconsistency: \
263                 chain must contain required overlap blocks \
264                 or query must only have one address",
265            );
266        }
267    }
268
269    Ok(chain.partial_transparent_tx_ids(addresses, query_height_range))
270}
271
272/// Returns the combined finalized and non-finalized transaction IDs.
273fn apply_tx_id_changes(
274    finalized_tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
275    chain_tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
276) -> BTreeMap<TransactionLocation, transaction::Hash> {
277    // Correctness: compensate for inconsistent tx IDs finalized blocks across multiple addresses,
278    // by combining them with overlapping non-finalized block tx IDs.
279    finalized_tx_ids.into_iter().chain(chain_tx_ids).collect()
280}