1use 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
28pub const ADDRESS_HEIGHTS_FULL_RANGE: RangeInclusive<Height> = Height(1)..=Height::MAX;
33
34#[derive(Clone, Debug, Default, Eq, PartialEq)]
37pub struct AddressUtxos {
38 utxos: BTreeMap<OutputLocation, transparent::Output>,
40
41 tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
43
44 network: Network,
46}
47
48impl AddressUtxos {
49 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 #[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
92pub 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 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 let chain_utxo_changes =
128 chain_transparent_utxo_changes(chain.as_ref(), &addresses, finalized_tip_range);
129
130 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
172fn finalized_address_utxos(
179 db: &ZebraDb,
180 addresses: &HashSet<transparent::Address>,
181) -> (
182 BTreeMap<OutputLocation, transparent::Output>,
183 Option<RangeInclusive<Height>>,
184) {
185 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 None
203 };
204
205 (finalized_utxos, finalized_tip_range)
206}
207
208fn 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 let required_min_non_finalized_root = finalized_tip_range.start().0 + 1;
254
255 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 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 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 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 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
362fn 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 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
378fn 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 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 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}