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.
1314use std::{
15 collections::{BTreeMap, HashSet},
16 ops::RangeInclusive,
17};
1819use zebra_chain::{block::Height, transaction, transparent};
2021use crate::{
22 service::{
23 finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES,
24 },
25 BoxError, TransactionLocation,
26};
2728/// 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
41C: AsRef<Chain>,
42{
43let mut tx_id_error = None;
4445// 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
49for _ in 0..=FINALIZED_STATE_QUERY_RETRIES {
50let (finalized_tx_ids, finalized_tip_range) =
51 finalized_transparent_tx_ids(db, &addresses, query_height_range.clone());
5253// Apply the non-finalized tx ID changes.
54let 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 );
6061// If the tx IDs are valid, return them, otherwise, retry or return an error.
62match chain_tx_id_changes {
63Ok(chain_tx_id_changes) => {
64let tx_ids = apply_tx_id_changes(finalized_tx_ids, chain_tx_id_changes);
6566return Ok(tx_ids);
67 }
6869Err(error) => tx_id_error = Some(Err(error)),
70 }
71 }
7273 tx_id_error.expect("unexpected missing error: attempts should set error or return")
74}
7576/// 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>,
88Option<RangeInclusive<Height>>,
89) {
90// # Correctness
91 //
92 // The StateService can commit additional blocks while we are querying transaction IDs.
9394 // Check if the finalized state changed while we were querying it
95let start_finalized_tip = db.finalized_tip_height();
9697let finalized_tx_ids = db.partial_finalized_transparent_tx_ids(addresses, query_height_range);
9899let end_finalized_tip = db.finalized_tip_height();
100101let finalized_tip_range = if let (Some(start_finalized_tip), Some(end_finalized_tip)) =
102 (start_finalized_tip, end_finalized_tip)
103 {
104Some(start_finalized_tip..=end_finalized_tip)
105 } else {
106// State is empty
107None
108};
109110 (finalized_tx_ids, finalized_tip_range)
111}
112113/// 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
126C: AsRef<Chain>,
127{
128let address_count = addresses.len();
129130let finalized_tip_range = match finalized_tip_range {
131Some(finalized_tip_range) => finalized_tip_range,
132None => {
133assert!(
134 chain.is_none(),
135"unexpected non-finalized chain when finalized state is empty"
136);
137138debug!(
139?finalized_tip_range,
140?address_count,
141"chain address tx ID query: state is empty, no tx IDs available",
142 );
143144return Ok(Default::default());
145 }
146 };
147148// # 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.
156157 // Check if the finalized and non-finalized states match or overlap
158let required_min_non_finalized_root = finalized_tip_range.start().0 + 1;
159160// 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
163let finalized_tip_status = required_min_non_finalized_root..=finalized_tip_range.end().0;
164let finalized_tip_status = if finalized_tip_status.is_empty() {
165let finalized_tip_height = *finalized_tip_range.end();
166Ok(finalized_tip_height)
167 } else {
168let required_non_finalized_overlap = finalized_tip_status;
169Err(required_non_finalized_overlap)
170 };
171172if chain.is_none() {
173if address_count <= 1 || finalized_tip_status.is_ok() {
174debug!(
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 );
182183return Ok(Default::default());
184 } else {
185// We can't compensate for inconsistent database queries,
186 // because the non-finalized chain is empty.
187debug!(
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 );
195196return Err("unable to get tx IDs: \
197 state was committing a block, and non-finalized chain is empty"
198.into());
199 }
200 }
201202let chain = chain.unwrap();
203let chain = chain.as_ref();
204205let non_finalized_root = chain.non_finalized_root_height();
206let non_finalized_tip = chain.non_finalized_tip_height();
207208assert!(
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 );
212213match finalized_tip_status {
214Ok(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.
217if finalized_tip_height >= non_finalized_tip {
218debug!(
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 );
227228return Ok(Default::default());
229 }
230 }
231232Err(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.
235if address_count > 1 && *required_non_finalized_overlap.end() > non_finalized_tip.0 {
236debug!(
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 );
247248return 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 }
254255// 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.
257assert!(
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 }
268269Ok(chain.partial_transparent_tx_ids(addresses, query_height_range))
270}
271272/// 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.
279finalized_tx_ids.into_iter().chain(chain_tx_ids).collect()
280}