zebra_state/service/read/address/balance.rs
1//! Reading address balances.
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::{collections::HashSet, sync::Arc};
15
16use zebra_chain::{
17 amount::{self, Amount, NegativeAllowed, NonNegative},
18 block::Height,
19 transparent,
20};
21
22use crate::{
23 service::{
24 finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES,
25 },
26 BoxError,
27};
28
29/// Returns the total transparent balance and received balance for the supplied [`transparent::Address`]es.
30///
31/// If the addresses do not exist in the non-finalized `chain` or finalized `db`, returns zero.
32pub fn transparent_balance(
33 chain: Option<Arc<Chain>>,
34 db: &ZebraDb,
35 addresses: HashSet<transparent::Address>,
36) -> Result<(Amount<NonNegative>, u64), BoxError> {
37 let mut balance_result = finalized_transparent_balance(db, &addresses);
38
39 // Retry the finalized balance query if it was interrupted by a finalizing block
40 //
41 // TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn
42 for _ in 0..FINALIZED_STATE_QUERY_RETRIES {
43 if balance_result.is_ok() {
44 break;
45 }
46
47 balance_result = finalized_transparent_balance(db, &addresses);
48 }
49
50 let (mut balance, finalized_tip) = balance_result?;
51
52 // Apply the non-finalized balance changes
53 if let Some(chain) = chain {
54 let chain_balance_change =
55 chain_transparent_balance_change(chain, &addresses, finalized_tip);
56
57 balance = apply_balance_change(balance, chain_balance_change)?;
58 }
59
60 Ok(balance)
61}
62
63/// Returns the total transparent balance for `addresses` in the finalized chain,
64/// and the finalized tip height the balances were queried at.
65///
66/// If the addresses do not exist in the finalized `db`, returns zero.
67//
68// TODO: turn the return type into a struct?
69fn finalized_transparent_balance(
70 db: &ZebraDb,
71 addresses: &HashSet<transparent::Address>,
72) -> Result<((Amount<NonNegative>, u64), Option<Height>), BoxError> {
73 // # Correctness
74 //
75 // The StateService can commit additional blocks while we are querying address balances.
76
77 // Check if the finalized state changed while we were querying it
78 let original_finalized_tip = db.tip();
79
80 let finalized_balance = db.partial_finalized_transparent_balance(addresses);
81
82 let finalized_tip = db.tip();
83
84 if original_finalized_tip != finalized_tip {
85 // Correctness: Some balances might be from before the block, and some after
86 return Err("unable to get balance: state was committing a block".into());
87 }
88
89 let finalized_tip = finalized_tip.map(|(height, _hash)| height);
90
91 Ok((finalized_balance, finalized_tip))
92}
93
94/// Returns the total transparent balance change for `addresses` in the non-finalized chain,
95/// matching the balance for the `finalized_tip`.
96///
97/// If the addresses do not exist in the non-finalized `chain`, returns zero.
98fn chain_transparent_balance_change(
99 mut chain: Arc<Chain>,
100 addresses: &HashSet<transparent::Address>,
101 finalized_tip: Option<Height>,
102) -> (Amount<NegativeAllowed>, u64) {
103 // # Correctness
104 //
105 // Find the balance adjustment that corrects for overlapping finalized and non-finalized blocks.
106
107 // Check if the finalized and non-finalized states match
108 let required_chain_root = finalized_tip
109 .map(|tip| (tip + 1).unwrap())
110 .unwrap_or(Height(0));
111
112 let chain = Arc::make_mut(&mut chain);
113
114 assert!(
115 chain.non_finalized_root_height() <= required_chain_root,
116 "unexpected chain gap: the best chain is updated after its previous root is finalized"
117 );
118
119 let chain_tip = chain.non_finalized_tip_height();
120
121 // If we've already committed this entire chain, ignore its balance changes.
122 // This is more likely if the non-finalized state is just getting started.
123 if chain_tip < required_chain_root {
124 return (Amount::zero(), 0);
125 }
126
127 // Correctness: some balances might have duplicate creates or spends,
128 // so we pop root blocks from `chain` until the chain root is a child of the finalized tip.
129 while chain.non_finalized_root_height() < required_chain_root {
130 // TODO: just revert the transparent balances, to improve performance
131 chain.pop_root();
132 }
133
134 chain.partial_transparent_balance_change(addresses)
135}
136
137/// Add the supplied finalized and non-finalized balances together,
138/// and return the result.
139fn apply_balance_change(
140 (finalized_balance, finalized_received): (Amount<NonNegative>, u64),
141 (chain_balance_change, chain_received_change): (Amount<NegativeAllowed>, u64),
142) -> amount::Result<(Amount<NonNegative>, u64)> {
143 let balance = finalized_balance.constrain()? + chain_balance_change;
144 // Addresses could receive more than the max money supply by sending to themselves,
145 // use u64::MAX if the addition overflows.
146 let received = finalized_received.saturating_add(chain_received_change);
147 Ok((balance?.constrain()?, received))
148}