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.
1314use std::{collections::HashSet, sync::Arc};
1516use zebra_chain::{
17 amount::{self, Amount, NegativeAllowed, NonNegative},
18 block::Height,
19 transparent,
20};
2122use crate::{
23 service::{
24 finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES,
25 },
26 BoxError,
27};
2829/// 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> {
37let mut balance_result = finalized_transparent_balance(db, &addresses);
3839// 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
42for _ in 0..FINALIZED_STATE_QUERY_RETRIES {
43if balance_result.is_ok() {
44break;
45 }
4647 balance_result = finalized_transparent_balance(db, &addresses);
48 }
4950let (mut balance, finalized_tip) = balance_result?;
5152// Apply the non-finalized balance changes
53if let Some(chain) = chain {
54let chain_balance_change =
55 chain_transparent_balance_change(chain, &addresses, finalized_tip);
5657 balance = apply_balance_change(balance, chain_balance_change).expect(
58"unexpected amount overflow: value balances are valid, so partial sum should be valid",
59 );
60 }
6162Ok(balance)
63}
6465/// Returns the total transparent balance for `addresses` in the finalized chain,
66/// and the finalized tip height the balances were queried at.
67///
68/// If the addresses do not exist in the finalized `db`, returns zero.
69//
70// TODO: turn the return type into a struct?
71fn finalized_transparent_balance(
72 db: &ZebraDb,
73 addresses: &HashSet<transparent::Address>,
74) -> Result<((Amount<NonNegative>, u64), Option<Height>), BoxError> {
75// # Correctness
76 //
77 // The StateService can commit additional blocks while we are querying address balances.
7879 // Check if the finalized state changed while we were querying it
80let original_finalized_tip = db.tip();
8182let finalized_balance = db.partial_finalized_transparent_balance(addresses);
8384let finalized_tip = db.tip();
8586if original_finalized_tip != finalized_tip {
87// Correctness: Some balances might be from before the block, and some after
88return Err("unable to get balance: state was committing a block".into());
89 }
9091let finalized_tip = finalized_tip.map(|(height, _hash)| height);
9293Ok((finalized_balance, finalized_tip))
94}
9596/// Returns the total transparent balance change for `addresses` in the non-finalized chain,
97/// matching the balance for the `finalized_tip`.
98///
99/// If the addresses do not exist in the non-finalized `chain`, returns zero.
100fn chain_transparent_balance_change(
101mut chain: Arc<Chain>,
102 addresses: &HashSet<transparent::Address>,
103 finalized_tip: Option<Height>,
104) -> (Amount<NegativeAllowed>, u64) {
105// # Correctness
106 //
107 // Find the balance adjustment that corrects for overlapping finalized and non-finalized blocks.
108109 // Check if the finalized and non-finalized states match
110let required_chain_root = finalized_tip
111 .map(|tip| (tip + 1).unwrap())
112 .unwrap_or(Height(0));
113114let chain = Arc::make_mut(&mut chain);
115116assert!(
117 chain.non_finalized_root_height() <= required_chain_root,
118"unexpected chain gap: the best chain is updated after its previous root is finalized"
119);
120121let chain_tip = chain.non_finalized_tip_height();
122123// If we've already committed this entire chain, ignore its balance changes.
124 // This is more likely if the non-finalized state is just getting started.
125if chain_tip < required_chain_root {
126return (Amount::zero(), 0);
127 }
128129// Correctness: some balances might have duplicate creates or spends,
130 // so we pop root blocks from `chain` until the chain root is a child of the finalized tip.
131while chain.non_finalized_root_height() < required_chain_root {
132// TODO: just revert the transparent balances, to improve performance
133chain.pop_root();
134 }
135136 chain.partial_transparent_balance_change(addresses)
137}
138139/// Add the supplied finalized and non-finalized balances together,
140/// and return the result.
141fn apply_balance_change(
142 (finalized_balance, finalized_received): (Amount<NonNegative>, u64),
143 (chain_balance_change, chain_received_change): (Amount<NegativeAllowed>, u64),
144) -> amount::Result<(Amount<NonNegative>, u64)> {
145let balance = finalized_balance.constrain()? + chain_balance_change;
146// Addresses could receive more than the max money supply by sending to themselves,
147 // use u64::MAX if the addition overflows.
148let received = finalized_received.saturating_add(chain_received_change);
149Ok((balance?.constrain()?, received))
150}