1use std::{
15 collections::{BTreeMap, BTreeSet, HashSet},
16 ops::RangeInclusive,
17};
18
19use derive_getters::Getters;
20use zebra_chain::{
21 block::{self, Height},
22 parameters::Network,
23 transaction, transparent,
24};
25
26use crate::{
27 service::{
28 finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES,
29 },
30 BoxError, OutputLocation, TransactionLocation,
31};
32
33pub const ADDRESS_HEIGHTS_FULL_RANGE: RangeInclusive<Height> = Height(1)..=Height::MAX;
38
39#[derive(Clone, Debug, Default, Eq, PartialEq, Getters)]
42pub struct AddressUtxos {
43 #[getter(skip)]
45 utxos: BTreeMap<OutputLocation, transparent::Output>,
46
47 #[getter(skip)]
49 tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
50
51 #[getter(skip)]
53 network: Network,
54
55 last_height_and_hash: Option<(block::Height, block::Hash)>,
58}
59
60impl AddressUtxos {
61 pub fn new(
63 network: &Network,
64 utxos: BTreeMap<OutputLocation, transparent::Output>,
65 tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
66 last_height_and_hash: Option<(block::Height, block::Hash)>,
67 ) -> Self {
68 Self {
69 utxos,
70 tx_ids,
71 network: network.clone(),
72 last_height_and_hash,
73 }
74 }
75
76 #[allow(dead_code)]
81 pub fn utxos(
82 &self,
83 ) -> impl Iterator<
84 Item = (
85 transparent::Address,
86 &transaction::Hash,
87 &OutputLocation,
88 &transparent::Output,
89 ),
90 > {
91 self.utxos.iter().map(|(out_loc, output)| {
92 (
93 output
94 .address(&self.network)
95 .expect("address indexes only contain outputs with addresses"),
96 self.tx_ids
97 .get(&out_loc.transaction_location())
98 .expect("address indexes are consistent"),
99 out_loc,
100 output,
101 )
102 })
103 }
104}
105
106pub fn address_utxos<C>(
112 network: &Network,
113 chain: Option<C>,
114 db: &ZebraDb,
115 addresses: HashSet<transparent::Address>,
116) -> Result<AddressUtxos, BoxError>
117where
118 C: AsRef<Chain>,
119{
120 let mut utxo_error = None;
121 let address_count = addresses.len();
122
123 for attempt in 0..=FINALIZED_STATE_QUERY_RETRIES {
128 debug!(?attempt, ?address_count, "starting address UTXO query");
129
130 let (finalized_utxos, finalized_tip_range) = finalized_address_utxos(db, &addresses);
131
132 debug!(
133 finalized_utxo_count = ?finalized_utxos.len(),
134 ?finalized_tip_range,
135 ?address_count,
136 ?attempt,
137 "finalized address UTXO response",
138 );
139
140 let chain_utxo_changes =
142 chain_transparent_utxo_changes(chain.as_ref(), &addresses, finalized_tip_range);
143
144 match chain_utxo_changes {
146 Ok((created_chain_utxos, spent_chain_utxos, last_height)) => {
147 debug!(
148 chain_utxo_count = ?created_chain_utxos.len(),
149 chain_utxo_spent = ?spent_chain_utxos.len(),
150 ?address_count,
151 ?attempt,
152 "chain address UTXO response",
153 );
154
155 let utxos =
156 apply_utxo_changes(finalized_utxos, created_chain_utxos, spent_chain_utxos);
157 let tx_ids = lookup_tx_ids_for_utxos(chain.as_ref(), db, &addresses, &utxos);
158
159 debug!(
160 full_utxo_count = ?utxos.len(),
161 tx_id_count = ?tx_ids.len(),
162 ?address_count,
163 ?attempt,
164 "full address UTXO response",
165 );
166
167 let last_height_and_hash = last_height.and_then(|height| {
169 chain
170 .as_ref()
171 .and_then(|c| c.as_ref().hash_by_height(height))
172 .or_else(|| db.hash(height))
173 .map(|hash| (height, hash))
174 });
175
176 return Ok(AddressUtxos::new(
177 network,
178 utxos,
179 tx_ids,
180 last_height_and_hash,
181 ));
182 }
183
184 Err(chain_utxo_error) => {
185 debug!(
186 ?chain_utxo_error,
187 ?address_count,
188 ?attempt,
189 "chain address UTXO response",
190 );
191
192 utxo_error = Some(Err(chain_utxo_error))
193 }
194 }
195 }
196
197 utxo_error.expect("unexpected missing error: attempts should set error or return")
198}
199
200fn finalized_address_utxos(
207 db: &ZebraDb,
208 addresses: &HashSet<transparent::Address>,
209) -> (
210 BTreeMap<OutputLocation, transparent::Output>,
211 Option<RangeInclusive<Height>>,
212) {
213 let start_finalized_tip = db.finalized_tip_height();
219
220 let finalized_utxos = db.partial_finalized_address_utxos(addresses);
221
222 let end_finalized_tip = db.finalized_tip_height();
223
224 let finalized_tip_range = if let (Some(start_finalized_tip), Some(end_finalized_tip)) =
225 (start_finalized_tip, end_finalized_tip)
226 {
227 Some(start_finalized_tip..=end_finalized_tip)
228 } else {
229 None
231 };
232
233 (finalized_utxos, finalized_tip_range)
234}
235
236fn chain_transparent_utxo_changes<C>(
246 chain: Option<C>,
247 addresses: &HashSet<transparent::Address>,
248 finalized_tip_range: Option<RangeInclusive<Height>>,
249) -> Result<
250 (
251 BTreeMap<OutputLocation, transparent::Output>,
252 BTreeSet<OutputLocation>,
253 Option<Height>,
254 ),
255 BoxError,
256>
257where
258 C: AsRef<Chain>,
259{
260 let address_count = addresses.len();
261
262 let finalized_tip_range = match finalized_tip_range {
263 Some(finalized_tip_range) => finalized_tip_range,
264 None => {
265 assert!(
266 chain.is_none(),
267 "unexpected non-finalized chain when finalized state is empty"
268 );
269
270 debug!(
271 ?finalized_tip_range,
272 ?address_count,
273 "chain address UTXO query: state is empty, no UTXOs available",
274 );
275
276 return Ok(Default::default());
277 }
278 };
279
280 let required_min_non_finalized_root = finalized_tip_range.start().0 + 1;
286
287 let finalized_tip_status = required_min_non_finalized_root..=finalized_tip_range.end().0;
291 let finalized_tip_status = if finalized_tip_status.is_empty() {
292 let finalized_tip_height = *finalized_tip_range.end();
293 Ok(finalized_tip_height)
294 } else {
295 let required_non_finalized_overlap = finalized_tip_status;
296 Err(required_non_finalized_overlap)
297 };
298
299 if chain.is_none() {
300 if let Ok(finalized_tip_height) = finalized_tip_status {
301 debug!(
302 ?finalized_tip_status,
303 ?required_min_non_finalized_root,
304 ?finalized_tip_range,
305 ?address_count,
306 "chain address UTXO query: \
307 finalized chain is consistent, and non-finalized chain is empty",
308 );
309
310 return Ok((
311 Default::default(),
312 Default::default(),
313 Some(finalized_tip_height),
314 ));
315 } else {
316 debug!(
319 ?finalized_tip_status,
320 ?required_min_non_finalized_root,
321 ?finalized_tip_range,
322 ?address_count,
323 "chain address UTXO query: \
324 finalized tip query was inconsistent, but non-finalized chain is empty",
325 );
326
327 return Err("unable to get UTXOs: \
328 state was committing a block, and non-finalized chain is empty"
329 .into());
330 }
331 }
332
333 let chain = chain.unwrap();
334 let chain = chain.as_ref();
335
336 let non_finalized_root = chain.non_finalized_root_height();
337 let non_finalized_tip = chain.non_finalized_tip_height();
338
339 assert!(
340 non_finalized_root.0 <= required_min_non_finalized_root,
341 "unexpected chain gap: the best chain is updated after its previous root is finalized",
342 );
343
344 match finalized_tip_status {
345 Ok(finalized_tip_height) => {
346 if finalized_tip_height >= non_finalized_tip {
349 debug!(
350 ?non_finalized_root,
351 ?non_finalized_tip,
352 ?finalized_tip_status,
353 ?finalized_tip_range,
354 ?address_count,
355 "chain address UTXO query: \
356 non-finalized blocks have all been finalized, no new UTXO changes",
357 );
358
359 return Ok((
360 Default::default(),
361 Default::default(),
362 Some(finalized_tip_height),
363 ));
364 }
365 }
366
367 Err(ref required_non_finalized_overlap) => {
368 if *required_non_finalized_overlap.end() > non_finalized_tip.0 {
371 debug!(
372 ?non_finalized_root,
373 ?non_finalized_tip,
374 ?finalized_tip_status,
375 ?finalized_tip_range,
376 ?address_count,
377 "chain address UTXO query: \
378 finalized tip query was inconsistent, \
379 and some inconsistent blocks are missing from the non-finalized chain",
380 );
381
382 return Err("unable to get UTXOs: \
383 state was committing a block, \
384 that is missing from the non-finalized chain"
385 .into());
386 }
387
388 assert!(
391 required_non_finalized_overlap
392 .clone()
393 .all(|height| chain.blocks.contains_key(&Height(height))),
394 "UTXO query inconsistency: chain must contain required overlap blocks",
395 );
396 }
397 }
398 let (created, spent) = chain.partial_transparent_utxo_changes(addresses);
399 Ok((created, spent, Some(non_finalized_tip)))
400}
401
402fn apply_utxo_changes(
405 finalized_utxos: BTreeMap<OutputLocation, transparent::Output>,
406 created_chain_utxos: BTreeMap<OutputLocation, transparent::Output>,
407 spent_chain_utxos: BTreeSet<OutputLocation>,
408) -> BTreeMap<OutputLocation, transparent::Output> {
409 finalized_utxos
412 .into_iter()
413 .chain(created_chain_utxos)
414 .filter(|(utxo_location, _output)| !spent_chain_utxos.contains(utxo_location))
415 .collect()
416}
417
418fn lookup_tx_ids_for_utxos<C>(
425 chain: Option<C>,
426 db: &ZebraDb,
427 addresses: &HashSet<transparent::Address>,
428 utxos: &BTreeMap<OutputLocation, transparent::Output>,
429) -> BTreeMap<TransactionLocation, transaction::Hash>
430where
431 C: AsRef<Chain>,
432{
433 let transaction_locations: BTreeSet<TransactionLocation> = utxos
435 .keys()
436 .map(|output_location| output_location.transaction_location())
437 .collect();
438
439 let chain_tx_ids = chain
440 .as_ref()
441 .map(|chain| {
442 chain
443 .as_ref()
444 .partial_transparent_tx_ids(addresses, ADDRESS_HEIGHTS_FULL_RANGE)
445 })
446 .unwrap_or_default();
447
448 transaction_locations
450 .iter()
451 .map(|tx_loc| {
452 (
453 *tx_loc,
454 chain_tx_ids.get(tx_loc).cloned().unwrap_or_else(|| {
455 db.transaction_hash(*tx_loc)
456 .expect("unexpected inconsistent UTXO indexes")
457 }),
458 )
459 })
460 .collect()
461}