use std::{
collections::{BTreeMap, HashSet},
ops::RangeInclusive,
};
use zebra_chain::{block::Height, transaction, transparent};
use crate::{
service::{
finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES,
},
BoxError, TransactionLocation,
};
pub fn transparent_tx_ids<C>(
chain: Option<C>,
db: &ZebraDb,
addresses: HashSet<transparent::Address>,
query_height_range: RangeInclusive<Height>,
) -> Result<BTreeMap<TransactionLocation, transaction::Hash>, BoxError>
where
C: AsRef<Chain>,
{
let mut tx_id_error = None;
for _ in 0..=FINALIZED_STATE_QUERY_RETRIES {
let (finalized_tx_ids, finalized_tip_range) =
finalized_transparent_tx_ids(db, &addresses, query_height_range.clone());
let chain_tx_id_changes = chain_transparent_tx_id_changes(
chain.as_ref(),
&addresses,
finalized_tip_range,
query_height_range.clone(),
);
match chain_tx_id_changes {
Ok(chain_tx_id_changes) => {
let tx_ids = apply_tx_id_changes(finalized_tx_ids, chain_tx_id_changes);
return Ok(tx_ids);
}
Err(error) => tx_id_error = Some(Err(error)),
}
}
tx_id_error.expect("unexpected missing error: attempts should set error or return")
}
fn finalized_transparent_tx_ids(
db: &ZebraDb,
addresses: &HashSet<transparent::Address>,
query_height_range: RangeInclusive<Height>,
) -> (
BTreeMap<TransactionLocation, transaction::Hash>,
Option<RangeInclusive<Height>>,
) {
let start_finalized_tip = db.finalized_tip_height();
let finalized_tx_ids = db.partial_finalized_transparent_tx_ids(addresses, query_height_range);
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_tx_ids, finalized_tip_range)
}
fn chain_transparent_tx_id_changes<C>(
chain: Option<C>,
addresses: &HashSet<transparent::Address>,
finalized_tip_range: Option<RangeInclusive<Height>>,
query_height_range: RangeInclusive<Height>,
) -> Result<BTreeMap<TransactionLocation, transaction::Hash>, 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 tx ID query: state is empty, no tx IDs 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 address_count <= 1 || finalized_tip_status.is_ok() {
debug!(
?finalized_tip_status,
?required_min_non_finalized_root,
?finalized_tip_range,
?address_count,
"chain address tx ID query: \
finalized chain is consistent, and non-finalized chain is empty",
);
return Ok(Default::default());
} else {
debug!(
?finalized_tip_status,
?required_min_non_finalized_root,
?finalized_tip_range,
?address_count,
"chain address tx ID query: \
finalized tip query was inconsistent, but non-finalized chain is empty",
);
return Err("unable to get tx IDs: \
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 tx ID query: \
non-finalized blocks have all been finalized, no new UTXO changes",
);
return Ok(Default::default());
}
}
Err(ref required_non_finalized_overlap) => {
if address_count > 1 && *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 tx ID query: \
finalized tip query was inconsistent, \
some inconsistent blocks are missing from the non-finalized chain, \
and the query has multiple addresses",
);
return Err("unable to get tx IDs: \
state was committing a block, \
that is missing from the non-finalized chain, \
and the query has multiple addresses"
.into());
}
assert!(
address_count <= 1
|| required_non_finalized_overlap
.clone()
.all(|height| chain.blocks.contains_key(&Height(height))),
"tx ID query inconsistency: \
chain must contain required overlap blocks \
or query must only have one address",
);
}
}
Ok(chain.partial_transparent_tx_ids(addresses, query_height_range))
}
fn apply_tx_id_changes(
finalized_tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
chain_tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
) -> BTreeMap<TransactionLocation, transaction::Hash> {
finalized_tx_ids.into_iter().chain(chain_tx_ids).collect()
}