use std::{
collections::{BTreeMap, BTreeSet, HashSet},
ops::RangeInclusive,
};
use derive_getters::Getters;
use zebra_chain::{
block::{self, Height},
parameters::Network,
transaction, transparent,
};
use crate::{
service::{
finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES,
},
BoxError, OutputLocation, TransactionLocation,
};
pub const ADDRESS_HEIGHTS_FULL_RANGE: RangeInclusive<Height> = Height(1)..=Height::MAX;
#[derive(Clone, Debug, Default, Eq, PartialEq, Getters)]
pub struct AddressUtxos {
#[getter(skip)]
utxos: BTreeMap<OutputLocation, transparent::Output>,
#[getter(skip)]
tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
#[getter(skip)]
network: Network,
last_height_and_hash: Option<(block::Height, block::Hash)>,
}
impl AddressUtxos {
pub fn new(
network: &Network,
utxos: BTreeMap<OutputLocation, transparent::Output>,
tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
last_height_and_hash: Option<(block::Height, block::Hash)>,
) -> Self {
Self {
utxos,
tx_ids,
network: network.clone(),
last_height_and_hash,
}
}
#[allow(dead_code)]
pub fn utxos(
&self,
) -> impl Iterator<
Item = (
transparent::Address,
&transaction::Hash,
&OutputLocation,
&transparent::Output,
),
> {
self.utxos.iter().map(|(out_loc, output)| {
(
output
.address(&self.network)
.expect("address indexes only contain outputs with addresses"),
self.tx_ids
.get(&out_loc.transaction_location())
.expect("address indexes are consistent"),
out_loc,
output,
)
})
}
}
pub fn address_utxos<C>(
network: &Network,
chain: Option<C>,
db: &ZebraDb,
addresses: HashSet<transparent::Address>,
) -> Result<AddressUtxos, BoxError>
where
C: AsRef<Chain>,
{
let mut utxo_error = None;
let address_count = addresses.len();
for attempt in 0..=FINALIZED_STATE_QUERY_RETRIES {
debug!(?attempt, ?address_count, "starting address UTXO query");
let (finalized_utxos, finalized_tip_range) = finalized_address_utxos(db, &addresses);
debug!(
finalized_utxo_count = ?finalized_utxos.len(),
?finalized_tip_range,
?address_count,
?attempt,
"finalized address UTXO response",
);
let chain_utxo_changes =
chain_transparent_utxo_changes(chain.as_ref(), &addresses, finalized_tip_range);
match chain_utxo_changes {
Ok((created_chain_utxos, spent_chain_utxos, last_height)) => {
debug!(
chain_utxo_count = ?created_chain_utxos.len(),
chain_utxo_spent = ?spent_chain_utxos.len(),
?address_count,
?attempt,
"chain address UTXO response",
);
let utxos =
apply_utxo_changes(finalized_utxos, created_chain_utxos, spent_chain_utxos);
let tx_ids = lookup_tx_ids_for_utxos(chain.as_ref(), db, &addresses, &utxos);
debug!(
full_utxo_count = ?utxos.len(),
tx_id_count = ?tx_ids.len(),
?address_count,
?attempt,
"full address UTXO response",
);
let last_height_and_hash = last_height.and_then(|height| {
chain
.as_ref()
.and_then(|c| c.as_ref().hash_by_height(height))
.or_else(|| db.hash(height))
.map(|hash| (height, hash))
});
return Ok(AddressUtxos::new(
network,
utxos,
tx_ids,
last_height_and_hash,
));
}
Err(chain_utxo_error) => {
debug!(
?chain_utxo_error,
?address_count,
?attempt,
"chain address UTXO response",
);
utxo_error = Some(Err(chain_utxo_error))
}
}
}
utxo_error.expect("unexpected missing error: attempts should set error or return")
}
fn finalized_address_utxos(
db: &ZebraDb,
addresses: &HashSet<transparent::Address>,
) -> (
BTreeMap<OutputLocation, transparent::Output>,
Option<RangeInclusive<Height>>,
) {
let start_finalized_tip = db.finalized_tip_height();
let finalized_utxos = db.partial_finalized_address_utxos(addresses);
let end_finalized_tip = db.finalized_tip_height();
let finalized_tip_range = if let (Some(start_finalized_tip), Some(end_finalized_tip)) =
(start_finalized_tip, end_finalized_tip)
{
Some(start_finalized_tip..=end_finalized_tip)
} else {
None
};
(finalized_utxos, finalized_tip_range)
}
fn chain_transparent_utxo_changes<C>(
chain: Option<C>,
addresses: &HashSet<transparent::Address>,
finalized_tip_range: Option<RangeInclusive<Height>>,
) -> Result<
(
BTreeMap<OutputLocation, transparent::Output>,
BTreeSet<OutputLocation>,
Option<Height>,
),
BoxError,
>
where
C: AsRef<Chain>,
{
let address_count = addresses.len();
let finalized_tip_range = match finalized_tip_range {
Some(finalized_tip_range) => finalized_tip_range,
None => {
assert!(
chain.is_none(),
"unexpected non-finalized chain when finalized state is empty"
);
debug!(
?finalized_tip_range,
?address_count,
"chain address UTXO query: state is empty, no UTXOs available",
);
return Ok(Default::default());
}
};
let required_min_non_finalized_root = finalized_tip_range.start().0 + 1;
let finalized_tip_status = required_min_non_finalized_root..=finalized_tip_range.end().0;
let finalized_tip_status = if finalized_tip_status.is_empty() {
let finalized_tip_height = *finalized_tip_range.end();
Ok(finalized_tip_height)
} else {
let required_non_finalized_overlap = finalized_tip_status;
Err(required_non_finalized_overlap)
};
if chain.is_none() {
if let Ok(finalized_tip_height) = finalized_tip_status {
debug!(
?finalized_tip_status,
?required_min_non_finalized_root,
?finalized_tip_range,
?address_count,
"chain address UTXO query: \
finalized chain is consistent, and non-finalized chain is empty",
);
return Ok((
Default::default(),
Default::default(),
Some(finalized_tip_height),
));
} else {
debug!(
?finalized_tip_status,
?required_min_non_finalized_root,
?finalized_tip_range,
?address_count,
"chain address UTXO query: \
finalized tip query was inconsistent, but non-finalized chain is empty",
);
return Err("unable to get UTXOs: \
state was committing a block, and non-finalized chain is empty"
.into());
}
}
let chain = chain.unwrap();
let chain = chain.as_ref();
let non_finalized_root = chain.non_finalized_root_height();
let non_finalized_tip = chain.non_finalized_tip_height();
assert!(
non_finalized_root.0 <= required_min_non_finalized_root,
"unexpected chain gap: the best chain is updated after its previous root is finalized",
);
match finalized_tip_status {
Ok(finalized_tip_height) => {
if finalized_tip_height >= non_finalized_tip {
debug!(
?non_finalized_root,
?non_finalized_tip,
?finalized_tip_status,
?finalized_tip_range,
?address_count,
"chain address UTXO query: \
non-finalized blocks have all been finalized, no new UTXO changes",
);
return Ok((
Default::default(),
Default::default(),
Some(finalized_tip_height),
));
}
}
Err(ref required_non_finalized_overlap) => {
if *required_non_finalized_overlap.end() > non_finalized_tip.0 {
debug!(
?non_finalized_root,
?non_finalized_tip,
?finalized_tip_status,
?finalized_tip_range,
?address_count,
"chain address UTXO query: \
finalized tip query was inconsistent, \
and some inconsistent blocks are missing from the non-finalized chain",
);
return Err("unable to get UTXOs: \
state was committing a block, \
that is missing from the non-finalized chain"
.into());
}
assert!(
required_non_finalized_overlap
.clone()
.all(|height| chain.blocks.contains_key(&Height(height))),
"UTXO query inconsistency: chain must contain required overlap blocks",
);
}
}
let (created, spent) = chain.partial_transparent_utxo_changes(addresses);
Ok((created, spent, Some(non_finalized_tip)))
}
fn apply_utxo_changes(
finalized_utxos: BTreeMap<OutputLocation, transparent::Output>,
created_chain_utxos: BTreeMap<OutputLocation, transparent::Output>,
spent_chain_utxos: BTreeSet<OutputLocation>,
) -> BTreeMap<OutputLocation, transparent::Output> {
finalized_utxos
.into_iter()
.chain(created_chain_utxos)
.filter(|(utxo_location, _output)| !spent_chain_utxos.contains(utxo_location))
.collect()
}
fn lookup_tx_ids_for_utxos<C>(
chain: Option<C>,
db: &ZebraDb,
addresses: &HashSet<transparent::Address>,
utxos: &BTreeMap<OutputLocation, transparent::Output>,
) -> BTreeMap<TransactionLocation, transaction::Hash>
where
C: AsRef<Chain>,
{
let transaction_locations: BTreeSet<TransactionLocation> = utxos
.keys()
.map(|output_location| output_location.transaction_location())
.collect();
let chain_tx_ids = chain
.as_ref()
.map(|chain| {
chain
.as_ref()
.partial_transparent_tx_ids(addresses, ADDRESS_HEIGHTS_FULL_RANGE)
})
.unwrap_or_default();
transaction_locations
.iter()
.map(|tx_loc| {
(
*tx_loc,
chain_tx_ids.get(tx_loc).cloned().unwrap_or_else(|| {
db.transaction_hash(*tx_loc)
.expect("unexpected inconsistent UTXO indexes")
}),
)
})
.collect()
}