zebra_state/service/read/address/
utxo.rs1use 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 Some(height).zip(
170 chain
171 .as_ref()
172 .and_then(|c| c.as_ref().hash_by_height(height))
173 .or_else(|| db.hash(height)),
174 )
175 });
176
177 return Ok(AddressUtxos::new(
178 network,
179 utxos,
180 tx_ids,
181 last_height_and_hash,
182 ));
183 }
184
185 Err(chain_utxo_error) => {
186 debug!(
187 ?chain_utxo_error,
188 ?address_count,
189 ?attempt,
190 "chain address UTXO response",
191 );
192
193 utxo_error = Some(Err(chain_utxo_error))
194 }
195 }
196 }
197
198 utxo_error.expect("unexpected missing error: attempts should set error or return")
199}
200
201fn finalized_address_utxos(
208 db: &ZebraDb,
209 addresses: &HashSet<transparent::Address>,
210) -> (
211 BTreeMap<OutputLocation, transparent::Output>,
212 Option<RangeInclusive<Height>>,
213) {
214 let start_finalized_tip = db.finalized_tip_height();
220
221 let finalized_utxos = db.partial_finalized_address_utxos(addresses);
222
223 let end_finalized_tip = db.finalized_tip_height();
224
225 let finalized_tip_range = if let (Some(start_finalized_tip), Some(end_finalized_tip)) =
226 (start_finalized_tip, end_finalized_tip)
227 {
228 Some(start_finalized_tip..=end_finalized_tip)
229 } else {
230 None
232 };
233
234 (finalized_utxos, finalized_tip_range)
235}
236
237fn chain_transparent_utxo_changes<C>(
247 chain: Option<C>,
248 addresses: &HashSet<transparent::Address>,
249 finalized_tip_range: Option<RangeInclusive<Height>>,
250) -> Result<
251 (
252 BTreeMap<OutputLocation, transparent::Output>,
253 BTreeSet<OutputLocation>,
254 Option<Height>,
255 ),
256 BoxError,
257>
258where
259 C: AsRef<Chain>,
260{
261 let address_count = addresses.len();
262
263 let finalized_tip_range = match finalized_tip_range {
264 Some(finalized_tip_range) => finalized_tip_range,
265 None => {
266 assert!(
267 chain.is_none(),
268 "unexpected non-finalized chain when finalized state is empty"
269 );
270
271 debug!(
272 ?finalized_tip_range,
273 ?address_count,
274 "chain address UTXO query: state is empty, no UTXOs available",
275 );
276
277 return Ok(Default::default());
278 }
279 };
280
281 let required_min_non_finalized_root = finalized_tip_range.start().0 + 1;
287
288 let finalized_tip_status = required_min_non_finalized_root..=finalized_tip_range.end().0;
292 let finalized_tip_status = if finalized_tip_status.is_empty() {
293 let finalized_tip_height = *finalized_tip_range.end();
294 Ok(finalized_tip_height)
295 } else {
296 let required_non_finalized_overlap = finalized_tip_status;
297 Err(required_non_finalized_overlap)
298 };
299
300 if chain.is_none() {
301 if let Ok(finalized_tip_height) = finalized_tip_status {
302 debug!(
303 ?finalized_tip_status,
304 ?required_min_non_finalized_root,
305 ?finalized_tip_range,
306 ?address_count,
307 "chain address UTXO query: \
308 finalized chain is consistent, and non-finalized chain is empty",
309 );
310
311 return Ok((
312 Default::default(),
313 Default::default(),
314 Some(finalized_tip_height),
315 ));
316 } else {
317 debug!(
320 ?finalized_tip_status,
321 ?required_min_non_finalized_root,
322 ?finalized_tip_range,
323 ?address_count,
324 "chain address UTXO query: \
325 finalized tip query was inconsistent, but non-finalized chain is empty",
326 );
327
328 return Err("unable to get UTXOs: \
329 state was committing a block, and non-finalized chain is empty"
330 .into());
331 }
332 }
333
334 let chain = chain.unwrap();
335 let chain = chain.as_ref();
336
337 let non_finalized_root = chain.non_finalized_root_height();
338 let non_finalized_tip = chain.non_finalized_tip_height();
339
340 assert!(
341 non_finalized_root.0 <= required_min_non_finalized_root,
342 "unexpected chain gap: the best chain is updated after its previous root is finalized",
343 );
344
345 match finalized_tip_status {
346 Ok(finalized_tip_height) => {
347 if finalized_tip_height >= non_finalized_tip {
350 debug!(
351 ?non_finalized_root,
352 ?non_finalized_tip,
353 ?finalized_tip_status,
354 ?finalized_tip_range,
355 ?address_count,
356 "chain address UTXO query: \
357 non-finalized blocks have all been finalized, no new UTXO changes",
358 );
359
360 return Ok((
361 Default::default(),
362 Default::default(),
363 Some(finalized_tip_height),
364 ));
365 }
366 }
367
368 Err(ref required_non_finalized_overlap) => {
369 if *required_non_finalized_overlap.end() > non_finalized_tip.0 {
372 debug!(
373 ?non_finalized_root,
374 ?non_finalized_tip,
375 ?finalized_tip_status,
376 ?finalized_tip_range,
377 ?address_count,
378 "chain address UTXO query: \
379 finalized tip query was inconsistent, \
380 and some inconsistent blocks are missing from the non-finalized chain",
381 );
382
383 return Err("unable to get UTXOs: \
384 state was committing a block, \
385 that is missing from the non-finalized chain"
386 .into());
387 }
388
389 assert!(
392 required_non_finalized_overlap
393 .clone()
394 .all(|height| chain.blocks.contains_key(&Height(height))),
395 "UTXO query inconsistency: chain must contain required overlap blocks",
396 );
397 }
398 }
399 let (created, spent) = chain.partial_transparent_utxo_changes(addresses);
400 Ok((created, spent, Some(non_finalized_tip)))
401}
402
403fn apply_utxo_changes(
406 finalized_utxos: BTreeMap<OutputLocation, transparent::Output>,
407 created_chain_utxos: BTreeMap<OutputLocation, transparent::Output>,
408 spent_chain_utxos: BTreeSet<OutputLocation>,
409) -> BTreeMap<OutputLocation, transparent::Output> {
410 finalized_utxos
413 .into_iter()
414 .chain(created_chain_utxos)
415 .filter(|(utxo_location, _output)| !spent_chain_utxos.contains(utxo_location))
416 .collect()
417}
418
419fn lookup_tx_ids_for_utxos<C>(
426 chain: Option<C>,
427 db: &ZebraDb,
428 addresses: &HashSet<transparent::Address>,
429 utxos: &BTreeMap<OutputLocation, transparent::Output>,
430) -> BTreeMap<TransactionLocation, transaction::Hash>
431where
432 C: AsRef<Chain>,
433{
434 let transaction_locations: BTreeSet<TransactionLocation> = utxos
436 .keys()
437 .map(|output_location| output_location.transaction_location())
438 .collect();
439
440 let chain_tx_ids = chain
441 .as_ref()
442 .map(|chain| {
443 chain
444 .as_ref()
445 .partial_transparent_tx_ids(addresses, ADDRESS_HEIGHTS_FULL_RANGE)
446 })
447 .unwrap_or_default();
448
449 transaction_locations
451 .iter()
452 .map(|tx_loc| {
453 (
454 *tx_loc,
455 chain_tx_ids.get(tx_loc).cloned().unwrap_or_else(|| {
456 db.transaction_hash(*tx_loc)
457 .expect("unexpected inconsistent UTXO indexes")
458 }),
459 )
460 })
461 .collect()
462}