WIP: Remove partial transparent stuff
This commit is contained in:
parent
6f23d0e95b
commit
92f2966e25
|
|
@ -935,31 +935,6 @@ pub enum ReadRequest {
|
||||||
limit: Option<NoteCommitmentSubtreeIndex>,
|
limit: Option<NoteCommitmentSubtreeIndex>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Looks up the balance of a set of transparent addresses.
|
|
||||||
///
|
|
||||||
/// Returns an [`Amount`](zebra_chain::amount::Amount) with the total
|
|
||||||
/// balance of the set of addresses.
|
|
||||||
AddressBalance(HashSet<transparent::Address>),
|
|
||||||
|
|
||||||
/// Looks up transaction hashes that were sent or received from addresses,
|
|
||||||
/// in an inclusive blockchain height range.
|
|
||||||
///
|
|
||||||
/// Returns
|
|
||||||
///
|
|
||||||
/// * An ordered, unique map of transaction locations and hashes.
|
|
||||||
/// * An empty map if no transactions were found for the given arguments.
|
|
||||||
///
|
|
||||||
/// Returned txids are in the order they appear in blocks,
|
|
||||||
/// which ensures that they are topologically sorted
|
|
||||||
/// (i.e. parent txids will appear before child txids).
|
|
||||||
TransactionIdsByAddresses {
|
|
||||||
/// The requested addresses.
|
|
||||||
addresses: HashSet<transparent::Address>,
|
|
||||||
|
|
||||||
/// The blocks to be queried for transactions.
|
|
||||||
height_range: RangeInclusive<block::Height>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Looks up utxos for the provided addresses.
|
/// Looks up utxos for the provided addresses.
|
||||||
///
|
///
|
||||||
/// Returns a type with found utxos and transaction information.
|
/// Returns a type with found utxos and transaction information.
|
||||||
|
|
@ -1032,8 +1007,6 @@ impl ReadRequest {
|
||||||
ReadRequest::OrchardTree { .. } => "orchard_tree",
|
ReadRequest::OrchardTree { .. } => "orchard_tree",
|
||||||
ReadRequest::SaplingSubtrees { .. } => "sapling_subtrees",
|
ReadRequest::SaplingSubtrees { .. } => "sapling_subtrees",
|
||||||
ReadRequest::OrchardSubtrees { .. } => "orchard_subtrees",
|
ReadRequest::OrchardSubtrees { .. } => "orchard_subtrees",
|
||||||
ReadRequest::AddressBalance { .. } => "address_balance",
|
|
||||||
ReadRequest::TransactionIdsByAddresses { .. } => "transaction_ids_by_addesses",
|
|
||||||
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
|
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
|
||||||
ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => {
|
ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => {
|
||||||
"best_chain_tip_nullifiers_anchors"
|
"best_chain_tip_nullifiers_anchors"
|
||||||
|
|
|
||||||
|
|
@ -1616,64 +1616,6 @@ impl Service<ReadRequest> for ReadStateService {
|
||||||
.wait_for_panics()
|
.wait_for_panics()
|
||||||
}
|
}
|
||||||
|
|
||||||
// For the get_address_balance RPC.
|
|
||||||
ReadRequest::AddressBalance(addresses) => {
|
|
||||||
let state = self.clone();
|
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
span.in_scope(move || {
|
|
||||||
let balance = state.non_finalized_state_receiver.with_watch_data(
|
|
||||||
|non_finalized_state| {
|
|
||||||
read::transparent_balance(
|
|
||||||
non_finalized_state.best_chain().cloned(),
|
|
||||||
&state.db,
|
|
||||||
addresses,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// The work is done in the future.
|
|
||||||
timer.finish(module_path!(), line!(), "ReadRequest::AddressBalance");
|
|
||||||
|
|
||||||
Ok(ReadResponse::AddressBalance(balance))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.wait_for_panics()
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the get_address_tx_ids RPC.
|
|
||||||
ReadRequest::TransactionIdsByAddresses {
|
|
||||||
addresses,
|
|
||||||
height_range,
|
|
||||||
} => {
|
|
||||||
let state = self.clone();
|
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
span.in_scope(move || {
|
|
||||||
let tx_ids = state.non_finalized_state_receiver.with_watch_data(
|
|
||||||
|non_finalized_state| {
|
|
||||||
read::transparent_tx_ids(
|
|
||||||
non_finalized_state.best_chain(),
|
|
||||||
&state.db,
|
|
||||||
addresses,
|
|
||||||
height_range,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// The work is done in the future.
|
|
||||||
timer.finish(
|
|
||||||
module_path!(),
|
|
||||||
line!(),
|
|
||||||
"ReadRequest::TransactionIdsByAddresses",
|
|
||||||
);
|
|
||||||
|
|
||||||
tx_ids.map(ReadResponse::AddressesTransactionIds)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.wait_for_panics()
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the get_address_utxos RPC.
|
// For the get_address_utxos RPC.
|
||||||
ReadRequest::UtxosByAddresses(addresses) => {
|
ReadRequest::UtxosByAddresses(addresses) => {
|
||||||
let state = self.clone();
|
let state = self.clone();
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ use crate::{
|
||||||
TransactionLocation, ValidateContextError,
|
TransactionLocation, ValidateContextError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::index::TransparentTransfers;
|
|
||||||
|
|
||||||
pub mod index;
|
pub mod index;
|
||||||
|
|
||||||
|
|
@ -181,13 +180,6 @@ pub struct ChainInner {
|
||||||
pub(crate) sapling_nullifiers: HashSet<sapling::Nullifier>,
|
pub(crate) sapling_nullifiers: HashSet<sapling::Nullifier>,
|
||||||
/// The Orchard nullifiers revealed by `blocks`.
|
/// The Orchard nullifiers revealed by `blocks`.
|
||||||
pub(crate) orchard_nullifiers: HashSet<orchard::Nullifier>,
|
pub(crate) orchard_nullifiers: HashSet<orchard::Nullifier>,
|
||||||
|
|
||||||
// Transparent Transfers
|
|
||||||
// TODO: move to the transparent section
|
|
||||||
//
|
|
||||||
/// Partial transparent address index data from `blocks`.
|
|
||||||
pub(super) partial_transparent_transfers: HashMap<transparent::Address, TransparentTransfers>,
|
|
||||||
|
|
||||||
// Chain Work
|
// Chain Work
|
||||||
//
|
//
|
||||||
/// The cumulative work represented by `blocks`.
|
/// The cumulative work represented by `blocks`.
|
||||||
|
|
@ -240,7 +232,6 @@ impl Chain {
|
||||||
sprout_nullifiers: Default::default(),
|
sprout_nullifiers: Default::default(),
|
||||||
sapling_nullifiers: Default::default(),
|
sapling_nullifiers: Default::default(),
|
||||||
orchard_nullifiers: Default::default(),
|
orchard_nullifiers: Default::default(),
|
||||||
partial_transparent_transfers: Default::default(),
|
|
||||||
partial_cumulative_work: Default::default(),
|
partial_cumulative_work: Default::default(),
|
||||||
history_trees_by_height: Default::default(),
|
history_trees_by_height: Default::default(),
|
||||||
chain_value_pools: finalized_tip_chain_value_pools,
|
chain_value_pools: finalized_tip_chain_value_pools,
|
||||||
|
|
@ -1247,116 +1238,6 @@ impl Chain {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address index queries
|
// Address index queries
|
||||||
|
|
||||||
/// Returns the transparent transfers for `addresses` in this non-finalized chain.
|
|
||||||
///
|
|
||||||
/// If none of the addresses have an address index, returns an empty iterator.
|
|
||||||
///
|
|
||||||
/// # Correctness
|
|
||||||
///
|
|
||||||
/// Callers should apply the returned indexes to the corresponding finalized state indexes.
|
|
||||||
///
|
|
||||||
/// The combined result will only be correct if the chains match.
|
|
||||||
/// The exact type of match varies by query.
|
|
||||||
pub fn partial_transparent_indexes<'a>(
|
|
||||||
&'a self,
|
|
||||||
addresses: &'a HashSet<transparent::Address>,
|
|
||||||
) -> impl Iterator<Item = &TransparentTransfers> {
|
|
||||||
addresses
|
|
||||||
.iter()
|
|
||||||
.flat_map(|address| self.partial_transparent_transfers.get(address))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the transparent balance change for `addresses` in this non-finalized chain.
|
|
||||||
///
|
|
||||||
/// If the balance doesn't change for any of the addresses, returns zero.
|
|
||||||
///
|
|
||||||
/// # Correctness
|
|
||||||
///
|
|
||||||
/// Callers should apply this balance change to the finalized state balance for `addresses`.
|
|
||||||
///
|
|
||||||
/// The total balance will only be correct if this partial chain matches the finalized state.
|
|
||||||
/// Specifically, the root of this partial chain must be a child block of the finalized tip.
|
|
||||||
pub fn partial_transparent_balance_change(
|
|
||||||
&self,
|
|
||||||
addresses: &HashSet<transparent::Address>,
|
|
||||||
) -> Amount<NegativeAllowed> {
|
|
||||||
let balance_change: Result<Amount<NegativeAllowed>, _> = self
|
|
||||||
.partial_transparent_indexes(addresses)
|
|
||||||
.map(|transfers| transfers.balance())
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
balance_change.expect(
|
|
||||||
"unexpected amount overflow: value balances are valid, so partial sum should be valid",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the transparent UTXO changes for `addresses` in this non-finalized chain.
|
|
||||||
///
|
|
||||||
/// If the UTXOs don't change for any of the addresses, returns empty lists.
|
|
||||||
///
|
|
||||||
/// # Correctness
|
|
||||||
///
|
|
||||||
/// Callers should apply these non-finalized UTXO changes to the finalized state UTXOs.
|
|
||||||
///
|
|
||||||
/// The UTXOs will only be correct if the non-finalized chain matches or overlaps with
|
|
||||||
/// the finalized state.
|
|
||||||
///
|
|
||||||
/// Specifically, a block in the partial chain must be a child block of the finalized tip.
|
|
||||||
/// (But the child block does not have to be the partial chain root.)
|
|
||||||
pub fn partial_transparent_utxo_changes(
|
|
||||||
&self,
|
|
||||||
addresses: &HashSet<transparent::Address>,
|
|
||||||
) -> (
|
|
||||||
BTreeMap<OutputLocation, transparent::Output>,
|
|
||||||
BTreeSet<OutputLocation>,
|
|
||||||
) {
|
|
||||||
let created_utxos = self
|
|
||||||
.partial_transparent_indexes(addresses)
|
|
||||||
.flat_map(|transfers| transfers.created_utxos())
|
|
||||||
.map(|(out_loc, output)| (*out_loc, output.clone()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let spent_utxos = self
|
|
||||||
.partial_transparent_indexes(addresses)
|
|
||||||
.flat_map(|transfers| transfers.spent_utxos())
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(created_utxos, spent_utxos)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`transaction::Hash`]es used by `addresses` to receive or spend funds,
|
|
||||||
/// in the non-finalized chain, filtered using the `query_height_range`.
|
|
||||||
///
|
|
||||||
/// If none of the addresses receive or spend funds in this partial chain, returns an empty list.
|
|
||||||
///
|
|
||||||
/// # Correctness
|
|
||||||
///
|
|
||||||
/// Callers should combine these non-finalized transactions with the finalized state transactions.
|
|
||||||
///
|
|
||||||
/// The transaction IDs will only be correct if the non-finalized chain matches or overlaps with
|
|
||||||
/// the finalized state.
|
|
||||||
///
|
|
||||||
/// Specifically, a block in the partial chain must be a child block of the finalized tip.
|
|
||||||
/// (But the child block does not have to be the partial chain root.)
|
|
||||||
///
|
|
||||||
/// This condition does not apply if there is only one address.
|
|
||||||
/// Since address transactions are only appended by blocks,
|
|
||||||
/// and the finalized state query reads them in order,
|
|
||||||
/// it is impossible to get inconsistent transactions for a single address.
|
|
||||||
pub fn partial_transparent_tx_ids(
|
|
||||||
&self,
|
|
||||||
addresses: &HashSet<transparent::Address>,
|
|
||||||
query_height_range: RangeInclusive<Height>,
|
|
||||||
) -> BTreeMap<TransactionLocation, transaction::Hash> {
|
|
||||||
self.partial_transparent_indexes(addresses)
|
|
||||||
.flat_map(|transfers| {
|
|
||||||
transfers.tx_ids(&self.tx_loc_by_hash, query_height_range.clone())
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the chain tip with the `contextually_valid` block,
|
/// Update the chain tip with the `contextually_valid` block,
|
||||||
/// running note commitment tree updates in parallel with other updates.
|
/// running note commitment tree updates in parallel with other updates.
|
||||||
///
|
///
|
||||||
|
|
@ -1525,11 +1406,6 @@ impl Chain {
|
||||||
"transactions must be unique within a single chain"
|
"transactions must be unique within a single chain"
|
||||||
);
|
);
|
||||||
|
|
||||||
// add the utxos this produced
|
|
||||||
self.update_chain_tip_with(&(outputs, &transaction_hash, new_outputs))?;
|
|
||||||
// delete the utxos this consumed
|
|
||||||
self.update_chain_tip_with(&(inputs, &transaction_hash, spent_outputs))?;
|
|
||||||
|
|
||||||
// add the shielded data
|
// add the shielded data
|
||||||
self.update_chain_tip_with(joinsplit_data)?;
|
self.update_chain_tip_with(joinsplit_data)?;
|
||||||
self.update_chain_tip_with(sapling_shielded_data_per_spend_anchor)?;
|
self.update_chain_tip_with(sapling_shielded_data_per_spend_anchor)?;
|
||||||
|
|
@ -1676,11 +1552,6 @@ impl UpdateWith<ContextuallyVerifiedBlock> for Chain {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// remove the utxos this produced
|
|
||||||
self.revert_chain_with(&(outputs, transaction_hash, new_outputs), position);
|
|
||||||
// reset the utxos this consumed
|
|
||||||
self.revert_chain_with(&(inputs, transaction_hash, spent_outputs), position);
|
|
||||||
|
|
||||||
// TODO: move this to the history tree UpdateWith.revert...()?
|
// TODO: move this to the history tree UpdateWith.revert...()?
|
||||||
// remove `transaction.hash` from `tx_loc_by_hash`
|
// remove `transaction.hash` from `tx_loc_by_hash`
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -1708,219 +1579,6 @@ impl UpdateWith<ContextuallyVerifiedBlock> for Chain {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Created UTXOs
|
|
||||||
//
|
|
||||||
// TODO: replace arguments with a struct
|
|
||||||
impl
|
|
||||||
UpdateWith<(
|
|
||||||
// The outputs from a transaction in this block
|
|
||||||
&Vec<transparent::Output>,
|
|
||||||
// The hash of the transaction that the outputs are from
|
|
||||||
&transaction::Hash,
|
|
||||||
// The UTXOs for all outputs created by this transaction (or block)
|
|
||||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
|
||||||
)> for Chain
|
|
||||||
{
|
|
||||||
#[allow(clippy::unwrap_in_result)]
|
|
||||||
fn update_chain_tip_with(
|
|
||||||
&mut self,
|
|
||||||
&(created_outputs, creating_tx_hash, block_created_outputs): &(
|
|
||||||
&Vec<transparent::Output>,
|
|
||||||
&transaction::Hash,
|
|
||||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
|
||||||
),
|
|
||||||
) -> Result<(), ValidateContextError> {
|
|
||||||
for output_index in 0..created_outputs.len() {
|
|
||||||
let outpoint = transparent::OutPoint {
|
|
||||||
hash: *creating_tx_hash,
|
|
||||||
index: output_index.try_into().expect("valid indexes fit in u32"),
|
|
||||||
};
|
|
||||||
let created_utxo = block_created_outputs
|
|
||||||
.get(&outpoint)
|
|
||||||
.expect("new_outputs contains all created UTXOs");
|
|
||||||
|
|
||||||
// Update the chain's created UTXOs
|
|
||||||
let previous_entry = self.created_utxos.insert(outpoint, created_utxo.clone());
|
|
||||||
assert_eq!(
|
|
||||||
previous_entry, None,
|
|
||||||
"unexpected created output: duplicate update or duplicate UTXO",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the address index with this UTXO
|
|
||||||
if let Some(receiving_address) = created_utxo.utxo.output.address(&self.network) {
|
|
||||||
let address_transfers = self
|
|
||||||
.partial_transparent_transfers
|
|
||||||
.entry(receiving_address)
|
|
||||||
.or_default();
|
|
||||||
|
|
||||||
address_transfers.update_chain_tip_with(&(&outpoint, created_utxo))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn revert_chain_with(
|
|
||||||
&mut self,
|
|
||||||
&(created_outputs, creating_tx_hash, block_created_outputs): &(
|
|
||||||
&Vec<transparent::Output>,
|
|
||||||
&transaction::Hash,
|
|
||||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
|
||||||
),
|
|
||||||
position: RevertPosition,
|
|
||||||
) {
|
|
||||||
for output_index in 0..created_outputs.len() {
|
|
||||||
let outpoint = transparent::OutPoint {
|
|
||||||
hash: *creating_tx_hash,
|
|
||||||
index: output_index.try_into().expect("valid indexes fit in u32"),
|
|
||||||
};
|
|
||||||
let created_utxo = block_created_outputs
|
|
||||||
.get(&outpoint)
|
|
||||||
.expect("new_outputs contains all created UTXOs");
|
|
||||||
|
|
||||||
// Revert the chain's created UTXOs
|
|
||||||
let removed_entry = self.created_utxos.remove(&outpoint);
|
|
||||||
assert!(
|
|
||||||
removed_entry.is_some(),
|
|
||||||
"unexpected revert of created output: duplicate revert or duplicate UTXO",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Revert the address index for this UTXO
|
|
||||||
if let Some(receiving_address) = created_utxo.utxo.output.address(&self.network) {
|
|
||||||
let address_transfers = self
|
|
||||||
.partial_transparent_transfers
|
|
||||||
.get_mut(&receiving_address)
|
|
||||||
.expect("block has previously been applied to the chain");
|
|
||||||
|
|
||||||
address_transfers.revert_chain_with(&(&outpoint, created_utxo), position);
|
|
||||||
|
|
||||||
// Remove this transfer if it is now empty
|
|
||||||
if address_transfers.is_empty() {
|
|
||||||
self.partial_transparent_transfers
|
|
||||||
.remove(&receiving_address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transparent inputs
|
|
||||||
//
|
|
||||||
// TODO: replace arguments with a struct
|
|
||||||
impl
|
|
||||||
UpdateWith<(
|
|
||||||
// The inputs from a transaction in this block
|
|
||||||
&Vec<transparent::Input>,
|
|
||||||
// The hash of the transaction that the inputs are from
|
|
||||||
// (not the transaction the spent output was created by)
|
|
||||||
&transaction::Hash,
|
|
||||||
// The outputs for all inputs spent in this transaction (or block)
|
|
||||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
|
||||||
)> for Chain
|
|
||||||
{
|
|
||||||
fn update_chain_tip_with(
|
|
||||||
&mut self,
|
|
||||||
&(spending_inputs, spending_tx_hash, spent_outputs): &(
|
|
||||||
&Vec<transparent::Input>,
|
|
||||||
&transaction::Hash,
|
|
||||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
|
||||||
),
|
|
||||||
) -> Result<(), ValidateContextError> {
|
|
||||||
for spending_input in spending_inputs.iter() {
|
|
||||||
let spent_outpoint = if let Some(spent_outpoint) = spending_input.outpoint() {
|
|
||||||
spent_outpoint
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Index the spent outpoint in the chain
|
|
||||||
let first_spend = self.spent_utxos.insert(spent_outpoint);
|
|
||||||
assert!(
|
|
||||||
first_spend,
|
|
||||||
"unexpected duplicate spent output: should be checked earlier"
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: fix tests to supply correct spent outputs, then turn this into an expect()
|
|
||||||
let spent_output = if let Some(spent_output) = spent_outputs.get(&spent_outpoint) {
|
|
||||||
spent_output
|
|
||||||
} else if !cfg!(test) {
|
|
||||||
panic!("unexpected missing spent output: all spent outputs must be indexed");
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Index the spent output for the address
|
|
||||||
if let Some(spending_address) = spent_output.utxo.output.address(&self.network) {
|
|
||||||
let address_transfers = self
|
|
||||||
.partial_transparent_transfers
|
|
||||||
.entry(spending_address)
|
|
||||||
.or_default();
|
|
||||||
|
|
||||||
address_transfers.update_chain_tip_with(&(
|
|
||||||
spending_input,
|
|
||||||
spending_tx_hash,
|
|
||||||
spent_output,
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn revert_chain_with(
|
|
||||||
&mut self,
|
|
||||||
&(spending_inputs, spending_tx_hash, spent_outputs): &(
|
|
||||||
&Vec<transparent::Input>,
|
|
||||||
&transaction::Hash,
|
|
||||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
|
||||||
),
|
|
||||||
position: RevertPosition,
|
|
||||||
) {
|
|
||||||
for spending_input in spending_inputs.iter() {
|
|
||||||
let spent_outpoint = if let Some(spent_outpoint) = spending_input.outpoint() {
|
|
||||||
spent_outpoint
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Revert the spent outpoint in the chain
|
|
||||||
let spent_outpoint_was_removed = self.spent_utxos.remove(&spent_outpoint);
|
|
||||||
assert!(
|
|
||||||
spent_outpoint_was_removed,
|
|
||||||
"spent_utxos must be present if block was added to chain"
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: fix tests to supply correct spent outputs, then turn this into an expect()
|
|
||||||
let spent_output = if let Some(spent_output) = spent_outputs.get(&spent_outpoint) {
|
|
||||||
spent_output
|
|
||||||
} else if !cfg!(test) {
|
|
||||||
panic!(
|
|
||||||
"unexpected missing reverted spent output: all spent outputs must be indexed"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Revert the spent output for the address
|
|
||||||
if let Some(receiving_address) = spent_output.utxo.output.address(&self.network) {
|
|
||||||
let address_transfers = self
|
|
||||||
.partial_transparent_transfers
|
|
||||||
.get_mut(&receiving_address)
|
|
||||||
.expect("block has previously been applied to the chain");
|
|
||||||
|
|
||||||
address_transfers
|
|
||||||
.revert_chain_with(&(spending_input, spending_tx_hash, spent_output), position);
|
|
||||||
|
|
||||||
// Remove this transfer if it is now empty
|
|
||||||
if address_transfers.is_empty() {
|
|
||||||
self.partial_transparent_transfers
|
|
||||||
.remove(&receiving_address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UpdateWith<Option<transaction::JoinSplitData<Groth16Proof>>> for Chain {
|
impl UpdateWith<Option<transaction::JoinSplitData<Groth16Proof>>> for Chain {
|
||||||
#[instrument(skip(self, joinsplit_data))]
|
#[instrument(skip(self, joinsplit_data))]
|
||||||
fn update_chain_tip_with(
|
fn update_chain_tip_with(
|
||||||
|
|
|
||||||
|
|
@ -17,284 +17,6 @@ use crate::{OutputLocation, TransactionLocation, ValidateContextError};
|
||||||
|
|
||||||
use super::{RevertPosition, UpdateWith};
|
use super::{RevertPosition, UpdateWith};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct TransparentTransfers {
|
|
||||||
/// The partial chain balance for a transparent address.
|
|
||||||
balance: Amount<NegativeAllowed>,
|
|
||||||
|
|
||||||
/// The partial list of transactions that spent or received UTXOs to a transparent address.
|
|
||||||
///
|
|
||||||
/// Since transactions can only be added to this set, it does not need
|
|
||||||
/// special handling for
|
|
||||||
/// [`ReadStateService`](crate::service::ReadStateService) response
|
|
||||||
/// inconsistencies.
|
|
||||||
///
|
|
||||||
/// The `getaddresstxids` RPC needs these transaction IDs to be sorted in chain order.
|
|
||||||
tx_ids: MultiSet<transaction::Hash>,
|
|
||||||
|
|
||||||
/// The partial list of UTXOs received by a transparent address.
|
|
||||||
///
|
|
||||||
/// The `getaddressutxos` RPC doesn't need these transaction IDs to be sorted in chain order,
|
|
||||||
/// but it might in future. So Zebra does it anyway.
|
|
||||||
///
|
|
||||||
/// Optional TODOs:
|
|
||||||
/// - store `Utxo`s in the chain, and just store the created locations for this address
|
|
||||||
/// - if we add an OutputLocation to UTXO, remove this OutputLocation,
|
|
||||||
/// and use the inner OutputLocation to sort Utxos in chain order
|
|
||||||
created_utxos: BTreeMap<OutputLocation, transparent::Output>,
|
|
||||||
|
|
||||||
/// The partial list of UTXOs spent by a transparent address.
|
|
||||||
///
|
|
||||||
/// The `getaddressutxos` RPC doesn't need these transaction IDs to be sorted in chain order,
|
|
||||||
/// but it might in future. So Zebra does it anyway.
|
|
||||||
///
|
|
||||||
/// Optional TODO:
|
|
||||||
/// - store spent `Utxo`s by location in the chain, use the chain spent UTXOs to filter,
|
|
||||||
/// and stop storing spent UTXOs by address
|
|
||||||
spent_utxos: BTreeSet<OutputLocation>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// A created UTXO
|
|
||||||
//
|
|
||||||
// TODO: replace arguments with a struct
|
|
||||||
impl
|
|
||||||
UpdateWith<(
|
|
||||||
// The location of the UTXO
|
|
||||||
&transparent::OutPoint,
|
|
||||||
// The UTXO data
|
|
||||||
// Includes the location of the transaction that created the output
|
|
||||||
&transparent::OrderedUtxo,
|
|
||||||
)> for TransparentTransfers
|
|
||||||
{
|
|
||||||
#[allow(clippy::unwrap_in_result)]
|
|
||||||
fn update_chain_tip_with(
|
|
||||||
&mut self,
|
|
||||||
&(outpoint, created_utxo): &(&transparent::OutPoint, &transparent::OrderedUtxo),
|
|
||||||
) -> Result<(), ValidateContextError> {
|
|
||||||
self.balance = (self.balance
|
|
||||||
+ created_utxo
|
|
||||||
.utxo
|
|
||||||
.output
|
|
||||||
.value()
|
|
||||||
.constrain()
|
|
||||||
.expect("NonNegative values are always valid NegativeAllowed values"))
|
|
||||||
.expect("total UTXO value has already been checked");
|
|
||||||
|
|
||||||
let transaction_location = transaction_location(created_utxo);
|
|
||||||
let output_location = OutputLocation::from_outpoint(transaction_location, outpoint);
|
|
||||||
|
|
||||||
let previous_entry = self
|
|
||||||
.created_utxos
|
|
||||||
.insert(output_location, created_utxo.utxo.output.clone());
|
|
||||||
assert_eq!(
|
|
||||||
previous_entry, None,
|
|
||||||
"unexpected created output: duplicate update or duplicate UTXO",
|
|
||||||
);
|
|
||||||
|
|
||||||
self.tx_ids.insert(outpoint.hash);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn revert_chain_with(
|
|
||||||
&mut self,
|
|
||||||
&(outpoint, created_utxo): &(&transparent::OutPoint, &transparent::OrderedUtxo),
|
|
||||||
_position: RevertPosition,
|
|
||||||
) {
|
|
||||||
self.balance = (self.balance
|
|
||||||
- created_utxo
|
|
||||||
.utxo
|
|
||||||
.output
|
|
||||||
.value()
|
|
||||||
.constrain()
|
|
||||||
.expect("NonNegative values are always valid NegativeAllowed values"))
|
|
||||||
.expect("reversing previous balance changes is always valid");
|
|
||||||
|
|
||||||
let transaction_location = transaction_location(created_utxo);
|
|
||||||
let output_location = OutputLocation::from_outpoint(transaction_location, outpoint);
|
|
||||||
|
|
||||||
let removed_entry = self.created_utxos.remove(&output_location);
|
|
||||||
assert!(
|
|
||||||
removed_entry.is_some(),
|
|
||||||
"unexpected revert of created output: duplicate update or duplicate UTXO",
|
|
||||||
);
|
|
||||||
|
|
||||||
let tx_id_was_removed = self.tx_ids.remove(&outpoint.hash);
|
|
||||||
assert!(
|
|
||||||
tx_id_was_removed,
|
|
||||||
"unexpected revert of created output transaction: \
|
|
||||||
duplicate revert, or revert of an output that was never updated",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A transparent input
|
|
||||||
//
|
|
||||||
// TODO: replace arguments with a struct
|
|
||||||
impl
|
|
||||||
UpdateWith<(
|
|
||||||
// The transparent input data
|
|
||||||
&transparent::Input,
|
|
||||||
// The hash of the transaction the input is from
|
|
||||||
// (not the transaction the spent output was created by)
|
|
||||||
&transaction::Hash,
|
|
||||||
// The output spent by the input
|
|
||||||
// Includes the location of the transaction that created the output
|
|
||||||
&transparent::OrderedUtxo,
|
|
||||||
)> for TransparentTransfers
|
|
||||||
{
|
|
||||||
#[allow(clippy::unwrap_in_result)]
|
|
||||||
fn update_chain_tip_with(
|
|
||||||
&mut self,
|
|
||||||
&(spending_input, spending_tx_hash, spent_output): &(
|
|
||||||
&transparent::Input,
|
|
||||||
&transaction::Hash,
|
|
||||||
&transparent::OrderedUtxo,
|
|
||||||
),
|
|
||||||
) -> Result<(), ValidateContextError> {
|
|
||||||
// Spending a UTXO subtracts value from the balance
|
|
||||||
self.balance = (self.balance
|
|
||||||
- spent_output
|
|
||||||
.utxo
|
|
||||||
.output
|
|
||||||
.value()
|
|
||||||
.constrain()
|
|
||||||
.expect("NonNegative values are always valid NegativeAllowed values"))
|
|
||||||
.expect("total UTXO value has already been checked");
|
|
||||||
|
|
||||||
let spent_outpoint = spending_input.outpoint().expect("checked by caller");
|
|
||||||
|
|
||||||
let spent_output_tx_loc = transaction_location(spent_output);
|
|
||||||
let output_location = OutputLocation::from_outpoint(spent_output_tx_loc, &spent_outpoint);
|
|
||||||
let spend_was_inserted = self.spent_utxos.insert(output_location);
|
|
||||||
assert!(
|
|
||||||
spend_was_inserted,
|
|
||||||
"unexpected spent output: duplicate update or duplicate spend",
|
|
||||||
);
|
|
||||||
|
|
||||||
self.tx_ids.insert(*spending_tx_hash);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn revert_chain_with(
|
|
||||||
&mut self,
|
|
||||||
&(spending_input, spending_tx_hash, spent_output): &(
|
|
||||||
&transparent::Input,
|
|
||||||
&transaction::Hash,
|
|
||||||
&transparent::OrderedUtxo,
|
|
||||||
),
|
|
||||||
_position: RevertPosition,
|
|
||||||
) {
|
|
||||||
self.balance = (self.balance
|
|
||||||
+ spent_output
|
|
||||||
.utxo
|
|
||||||
.output
|
|
||||||
.value()
|
|
||||||
.constrain()
|
|
||||||
.expect("NonNegative values are always valid NegativeAllowed values"))
|
|
||||||
.expect("reversing previous balance changes is always valid");
|
|
||||||
|
|
||||||
let spent_outpoint = spending_input.outpoint().expect("checked by caller");
|
|
||||||
|
|
||||||
let spent_output_tx_loc = transaction_location(spent_output);
|
|
||||||
let output_location = OutputLocation::from_outpoint(spent_output_tx_loc, &spent_outpoint);
|
|
||||||
let spend_was_removed = self.spent_utxos.remove(&output_location);
|
|
||||||
assert!(
|
|
||||||
spend_was_removed,
|
|
||||||
"unexpected revert of spent output: \
|
|
||||||
duplicate revert, or revert of a spent output that was never updated",
|
|
||||||
);
|
|
||||||
|
|
||||||
let tx_id_was_removed = self.tx_ids.remove(spending_tx_hash);
|
|
||||||
assert!(
|
|
||||||
tx_id_was_removed,
|
|
||||||
"unexpected revert of spending input transaction: \
|
|
||||||
duplicate revert, or revert of an input that was never updated",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransparentTransfers {
|
|
||||||
/// Returns true if there are no transfers for this address.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.balance == Amount::<NegativeAllowed>::zero()
|
|
||||||
&& self.tx_ids.is_empty()
|
|
||||||
&& self.created_utxos.is_empty()
|
|
||||||
&& self.spent_utxos.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the partial balance for this address.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn balance(&self) -> Amount<NegativeAllowed> {
|
|
||||||
self.balance
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`transaction::Hash`]es of the transactions that sent or
|
|
||||||
/// received transparent transfers to this address, in this partial chain,
|
|
||||||
/// filtered by `query_height_range`.
|
|
||||||
///
|
|
||||||
/// The transactions are returned in chain order.
|
|
||||||
///
|
|
||||||
/// `chain_tx_loc_by_hash` should be the `tx_loc_by_hash` field from the
|
|
||||||
/// [`Chain`][1] containing this index.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// If `chain_tx_loc_by_hash` is missing some transaction hashes from this
|
|
||||||
/// index.
|
|
||||||
///
|
|
||||||
/// [1]: super::super::Chain
|
|
||||||
pub fn tx_ids(
|
|
||||||
&self,
|
|
||||||
chain_tx_loc_by_hash: &HashMap<transaction::Hash, TransactionLocation>,
|
|
||||||
query_height_range: RangeInclusive<Height>,
|
|
||||||
) -> BTreeMap<TransactionLocation, transaction::Hash> {
|
|
||||||
self.tx_ids
|
|
||||||
.distinct_elements()
|
|
||||||
.filter_map(|tx_hash| {
|
|
||||||
let tx_loc = *chain_tx_loc_by_hash
|
|
||||||
.get(tx_hash)
|
|
||||||
.expect("all hashes are indexed");
|
|
||||||
|
|
||||||
if query_height_range.contains(&tx_loc.height) {
|
|
||||||
Some((tx_loc, *tx_hash))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the new transparent outputs sent to this address,
|
|
||||||
/// in this partial chain, in chain order.
|
|
||||||
///
|
|
||||||
/// Some of these outputs might already be spent.
|
|
||||||
/// [`TransparentTransfers::spent_utxos`] returns spent UTXOs.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn created_utxos(&self) -> &BTreeMap<OutputLocation, transparent::Output> {
|
|
||||||
&self.created_utxos
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`OutputLocation`]s of the spent transparent outputs sent to this address,
|
|
||||||
/// in this partial chain, in chain order.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn spent_utxos(&self) -> &BTreeSet<OutputLocation> {
|
|
||||||
&self.spent_utxos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TransparentTransfers {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
balance: Amount::zero(),
|
|
||||||
tx_ids: Default::default(),
|
|
||||||
created_utxos: Default::default(),
|
|
||||||
spent_utxos: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the transaction location for an [`transparent::OrderedUtxo`].
|
/// Returns the transaction location for an [`transparent::OrderedUtxo`].
|
||||||
pub fn transaction_location(ordered_utxo: &transparent::OrderedUtxo) -> TransactionLocation {
|
pub fn transaction_location(ordered_utxo: &transparent::OrderedUtxo) -> TransactionLocation {
|
||||||
TransactionLocation::from_usize(ordered_utxo.utxo.height, ordered_utxo.tx_index_in_block)
|
TransactionLocation::from_usize(ordered_utxo.utxo.height, ordered_utxo.tx_index_in_block)
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ pub mod difficulty;
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub use address::{
|
pub use address::{
|
||||||
balance::transparent_balance,
|
|
||||||
tx_id::transparent_tx_ids,
|
|
||||||
utxo::{address_utxos, AddressUtxos},
|
utxo::{address_utxos, AddressUtxos},
|
||||||
};
|
};
|
||||||
pub use block::{
|
pub use block::{
|
||||||
|
|
|
||||||
|
|
@ -26,116 +26,6 @@ use crate::{
|
||||||
BoxError,
|
BoxError,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Returns the total transparent balance for the supplied [`transparent::Address`]es.
|
|
||||||
///
|
|
||||||
/// If the addresses do not exist in the non-finalized `chain` or finalized `db`, returns zero.
|
|
||||||
pub fn transparent_balance(
|
|
||||||
chain: Option<Arc<Chain>>,
|
|
||||||
db: &ZebraDb,
|
|
||||||
addresses: HashSet<transparent::Address>,
|
|
||||||
) -> Result<Amount<NonNegative>, BoxError> {
|
|
||||||
let mut balance_result = finalized_transparent_balance(db, &addresses);
|
|
||||||
|
|
||||||
// Retry the finalized balance query if it was interrupted by a finalizing block
|
|
||||||
//
|
|
||||||
// TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn
|
|
||||||
for _ in 0..FINALIZED_STATE_QUERY_RETRIES {
|
|
||||||
if balance_result.is_ok() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
balance_result = finalized_transparent_balance(db, &addresses);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (mut balance, finalized_tip) = balance_result?;
|
|
||||||
|
|
||||||
// Apply the non-finalized balance changes
|
|
||||||
if let Some(chain) = chain {
|
|
||||||
let chain_balance_change =
|
|
||||||
chain_transparent_balance_change(chain, &addresses, finalized_tip);
|
|
||||||
|
|
||||||
balance = apply_balance_change(balance, chain_balance_change).expect(
|
|
||||||
"unexpected amount overflow: value balances are valid, so partial sum should be valid",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(balance)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the total transparent balance for `addresses` in the finalized chain,
|
|
||||||
/// and the finalized tip height the balances were queried at.
|
|
||||||
///
|
|
||||||
/// If the addresses do not exist in the finalized `db`, returns zero.
|
|
||||||
//
|
|
||||||
// TODO: turn the return type into a struct?
|
|
||||||
fn finalized_transparent_balance(
|
|
||||||
db: &ZebraDb,
|
|
||||||
addresses: &HashSet<transparent::Address>,
|
|
||||||
) -> Result<(Amount<NonNegative>, Option<Height>), BoxError> {
|
|
||||||
// # Correctness
|
|
||||||
//
|
|
||||||
// The StateService can commit additional blocks while we are querying address balances.
|
|
||||||
|
|
||||||
// Check if the finalized state changed while we were querying it
|
|
||||||
let original_finalized_tip = db.tip();
|
|
||||||
|
|
||||||
let finalized_balance = db.partial_finalized_transparent_balance(addresses);
|
|
||||||
|
|
||||||
let finalized_tip = db.tip();
|
|
||||||
|
|
||||||
if original_finalized_tip != finalized_tip {
|
|
||||||
// Correctness: Some balances might be from before the block, and some after
|
|
||||||
return Err("unable to get balance: state was committing a block".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalized_tip = finalized_tip.map(|(height, _hash)| height);
|
|
||||||
|
|
||||||
Ok((finalized_balance, finalized_tip))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the total transparent balance change for `addresses` in the non-finalized chain,
|
|
||||||
/// matching the balance for the `finalized_tip`.
|
|
||||||
///
|
|
||||||
/// If the addresses do not exist in the non-finalized `chain`, returns zero.
|
|
||||||
fn chain_transparent_balance_change(
|
|
||||||
mut chain: Arc<Chain>,
|
|
||||||
addresses: &HashSet<transparent::Address>,
|
|
||||||
finalized_tip: Option<Height>,
|
|
||||||
) -> Amount<NegativeAllowed> {
|
|
||||||
// # Correctness
|
|
||||||
//
|
|
||||||
// Find the balance adjustment that corrects for overlapping finalized and non-finalized blocks.
|
|
||||||
|
|
||||||
// Check if the finalized and non-finalized states match
|
|
||||||
let required_chain_root = finalized_tip
|
|
||||||
.map(|tip| (tip + 1).unwrap())
|
|
||||||
.unwrap_or(Height(0));
|
|
||||||
|
|
||||||
let chain = Arc::make_mut(&mut chain);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
chain.non_finalized_root_height() <= required_chain_root,
|
|
||||||
"unexpected chain gap: the best chain is updated after its previous root is finalized"
|
|
||||||
);
|
|
||||||
|
|
||||||
let chain_tip = chain.non_finalized_tip_height();
|
|
||||||
|
|
||||||
// If we've already committed this entire chain, ignore its balance changes.
|
|
||||||
// This is more likely if the non-finalized state is just getting started.
|
|
||||||
if chain_tip < required_chain_root {
|
|
||||||
return Amount::zero();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Correctness: some balances might have duplicate creates or spends,
|
|
||||||
// so we pop root blocks from `chain` until the chain root is a child of the finalized tip.
|
|
||||||
while chain.non_finalized_root_height() < required_chain_root {
|
|
||||||
// TODO: just revert the transparent balances, to improve performance
|
|
||||||
chain.pop_root();
|
|
||||||
}
|
|
||||||
|
|
||||||
chain.partial_transparent_balance_change(addresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add the supplied finalized and non-finalized balances together,
|
/// Add the supplied finalized and non-finalized balances together,
|
||||||
/// and return the result.
|
/// and return the result.
|
||||||
fn apply_balance_change(
|
fn apply_balance_change(
|
||||||
|
|
|
||||||
|
|
@ -25,249 +25,6 @@ use crate::{
|
||||||
BoxError, TransactionLocation,
|
BoxError, TransactionLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Returns the transaction IDs that sent or received funds from the supplied [`transparent::Address`]es,
|
|
||||||
/// within `query_height_range`, in chain order.
|
|
||||||
///
|
|
||||||
/// If the addresses do not exist in the non-finalized `chain` or finalized `db`,
|
|
||||||
/// or the `query_height_range` is totally outside both the `chain` and `db` range,
|
|
||||||
/// returns an empty list.
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Retry the finalized tx ID query if it was interrupted by a finalizing block,
|
|
||||||
// and the non-finalized chain doesn't overlap the changed heights.
|
|
||||||
//
|
|
||||||
// TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn
|
|
||||||
for _ in 0..=FINALIZED_STATE_QUERY_RETRIES {
|
|
||||||
let (finalized_tx_ids, finalized_tip_range) =
|
|
||||||
finalized_transparent_tx_ids(db, &addresses, query_height_range.clone());
|
|
||||||
|
|
||||||
// Apply the non-finalized tx ID changes.
|
|
||||||
let chain_tx_id_changes = chain_transparent_tx_id_changes(
|
|
||||||
chain.as_ref(),
|
|
||||||
&addresses,
|
|
||||||
finalized_tip_range,
|
|
||||||
query_height_range.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the tx IDs are valid, return them, otherwise, retry or return an error.
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`transaction::Hash`]es for `addresses` in the finalized chain `query_height_range`,
|
|
||||||
/// and the finalized tip heights the transaction IDs were queried at.
|
|
||||||
///
|
|
||||||
/// If the addresses do not exist in the finalized `db`, returns an empty list.
|
|
||||||
//
|
|
||||||
// TODO: turn the return type into a struct?
|
|
||||||
fn finalized_transparent_tx_ids(
|
|
||||||
db: &ZebraDb,
|
|
||||||
addresses: &HashSet<transparent::Address>,
|
|
||||||
query_height_range: RangeInclusive<Height>,
|
|
||||||
) -> (
|
|
||||||
BTreeMap<TransactionLocation, transaction::Hash>,
|
|
||||||
Option<RangeInclusive<Height>>,
|
|
||||||
) {
|
|
||||||
// # Correctness
|
|
||||||
//
|
|
||||||
// The StateService can commit additional blocks while we are querying transaction IDs.
|
|
||||||
|
|
||||||
// Check if the finalized state changed while we were querying it
|
|
||||||
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 {
|
|
||||||
// State is empty
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
(finalized_tx_ids, finalized_tip_range)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the extra transaction IDs for `addresses` in the non-finalized chain `query_height_range`,
|
|
||||||
/// matching or overlapping the transaction IDs for the `finalized_tip_range`,
|
|
||||||
///
|
|
||||||
/// If the addresses do not exist in the non-finalized `chain`, returns an empty list.
|
|
||||||
//
|
|
||||||
// TODO: turn the return type into a struct?
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// # Correctness
|
|
||||||
//
|
|
||||||
// We can compensate for addresses with mismatching blocks,
|
|
||||||
// by adding the overlapping non-finalized transaction IDs.
|
|
||||||
//
|
|
||||||
// If there is only one address, mismatches aren't possible,
|
|
||||||
// because tx IDs are added to the finalized state in chain order (and never removed),
|
|
||||||
// and they are queried in chain order.
|
|
||||||
|
|
||||||
// Check if the finalized and non-finalized states match or overlap
|
|
||||||
let required_min_non_finalized_root = finalized_tip_range.start().0 + 1;
|
|
||||||
|
|
||||||
// Work out if we need to compensate for finalized query results from multiple heights:
|
|
||||||
// - Ok contains the finalized tip height (no need to compensate)
|
|
||||||
// - Err contains the required non-finalized chain overlap
|
|
||||||
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 {
|
|
||||||
// We can't compensate for inconsistent database queries,
|
|
||||||
// because the non-finalized chain is empty.
|
|
||||||
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 we've already committed this entire chain, ignore its UTXO changes.
|
|
||||||
// This is more likely if the non-finalized state is just getting started.
|
|
||||||
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) => {
|
|
||||||
// We can't compensate for inconsistent database queries,
|
|
||||||
// because the non-finalized chain is below the inconsistent query range.
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Correctness: some finalized UTXOs might have duplicate creates or spends,
|
|
||||||
// but we've just checked they can be corrected by applying the non-finalized UTXO changes.
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the combined finalized and non-finalized transaction IDs.
|
/// Returns the combined finalized and non-finalized transaction IDs.
|
||||||
fn apply_tx_id_changes(
|
fn apply_tx_id_changes(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue