From 92f2966e253cd2b6a526aa195f9e97c0e4c0c3d1 Mon Sep 17 00:00:00 2001 From: Likho Date: Tue, 30 Apr 2024 16:16:04 +0200 Subject: [PATCH] WIP: Remove partial transparent stuff --- zebra-state/src/request.rs | 27 -- zebra-state/src/service.rs | 58 --- .../src/service/non_finalized_state/chain.rs | 342 ------------------ .../non_finalized_state/chain/index.rs | 278 -------------- zebra-state/src/service/read.rs | 2 - .../src/service/read/address/balance.rs | 110 ------ zebra-state/src/service/read/address/tx_id.rs | 243 ------------- 7 files changed, 1060 deletions(-) diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 6f785a9d..9adba765 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -935,31 +935,6 @@ pub enum ReadRequest { limit: Option, }, - /// 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), - - /// 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, - - /// The blocks to be queried for transactions. - height_range: RangeInclusive, - }, - /// Looks up utxos for the provided addresses. /// /// Returns a type with found utxos and transaction information. @@ -1032,8 +1007,6 @@ impl ReadRequest { ReadRequest::OrchardTree { .. } => "orchard_tree", ReadRequest::SaplingSubtrees { .. } => "sapling_subtrees", ReadRequest::OrchardSubtrees { .. } => "orchard_subtrees", - ReadRequest::AddressBalance { .. } => "address_balance", - ReadRequest::TransactionIdsByAddresses { .. } => "transaction_ids_by_addesses", ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses", ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => { "best_chain_tip_nullifiers_anchors" diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 203c1d34..6f35d5fa 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1616,64 +1616,6 @@ impl Service for ReadStateService { .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. ReadRequest::UtxosByAddresses(addresses) => { let state = self.clone(); diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 45bbb026..01cfd527 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -33,7 +33,6 @@ use crate::{ TransactionLocation, ValidateContextError, }; -use self::index::TransparentTransfers; pub mod index; @@ -181,13 +180,6 @@ pub struct ChainInner { pub(crate) sapling_nullifiers: HashSet, /// The Orchard nullifiers revealed by `blocks`. pub(crate) orchard_nullifiers: HashSet, - - // Transparent Transfers - // TODO: move to the transparent section - // - /// Partial transparent address index data from `blocks`. - pub(super) partial_transparent_transfers: HashMap, - // Chain Work // /// The cumulative work represented by `blocks`. @@ -240,7 +232,6 @@ impl Chain { sprout_nullifiers: Default::default(), sapling_nullifiers: Default::default(), orchard_nullifiers: Default::default(), - partial_transparent_transfers: Default::default(), partial_cumulative_work: Default::default(), history_trees_by_height: Default::default(), chain_value_pools: finalized_tip_chain_value_pools, @@ -1247,116 +1238,6 @@ impl Chain { } // 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, - ) -> impl Iterator { - 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, - ) -> Amount { - let balance_change: Result, _> = 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, - ) -> ( - BTreeMap, - BTreeSet, - ) { - 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, - query_height_range: RangeInclusive, - ) -> BTreeMap { - 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, /// running note commitment tree updates in parallel with other updates. /// @@ -1525,11 +1406,6 @@ impl 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 self.update_chain_tip_with(joinsplit_data)?; self.update_chain_tip_with(sapling_shielded_data_per_spend_anchor)?; @@ -1676,11 +1552,6 @@ impl UpdateWith 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...()? // remove `transaction.hash` from `tx_loc_by_hash` assert!( @@ -1708,219 +1579,6 @@ impl UpdateWith for Chain { } } -// Created UTXOs -// -// TODO: replace arguments with a struct -impl - UpdateWith<( - // The outputs from a transaction in this block - &Vec, - // The hash of the transaction that the outputs are from - &transaction::Hash, - // The UTXOs for all outputs created by this transaction (or block) - &HashMap, - )> for Chain -{ - #[allow(clippy::unwrap_in_result)] - fn update_chain_tip_with( - &mut self, - &(created_outputs, creating_tx_hash, block_created_outputs): &( - &Vec, - &transaction::Hash, - &HashMap, - ), - ) -> 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, - &transaction::Hash, - &HashMap, - ), - 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, - // 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, - )> for Chain -{ - fn update_chain_tip_with( - &mut self, - &(spending_inputs, spending_tx_hash, spent_outputs): &( - &Vec, - &transaction::Hash, - &HashMap, - ), - ) -> 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, - &transaction::Hash, - &HashMap, - ), - 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>> for Chain { #[instrument(skip(self, joinsplit_data))] fn update_chain_tip_with( diff --git a/zebra-state/src/service/non_finalized_state/chain/index.rs b/zebra-state/src/service/non_finalized_state/chain/index.rs index 90e904fa..2852ff5e 100644 --- a/zebra-state/src/service/non_finalized_state/chain/index.rs +++ b/zebra-state/src/service/non_finalized_state/chain/index.rs @@ -17,284 +17,6 @@ use crate::{OutputLocation, TransactionLocation, ValidateContextError}; use super::{RevertPosition, UpdateWith}; -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct TransparentTransfers { - /// The partial chain balance for a transparent address. - balance: Amount, - - /// 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, - - /// 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, - - /// 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, -} - -// 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::::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 { - 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, - query_height_range: RangeInclusive, - ) -> BTreeMap { - 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 { - &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 { - &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`]. pub fn transaction_location(ordered_utxo: &transparent::OrderedUtxo) -> TransactionLocation { TransactionLocation::from_usize(ordered_utxo.utxo.height, ordered_utxo.tx_index_in_block) diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index b8d02239..824740ce 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -26,8 +26,6 @@ pub mod difficulty; mod tests; pub use address::{ - balance::transparent_balance, - tx_id::transparent_tx_ids, utxo::{address_utxos, AddressUtxos}, }; pub use block::{ diff --git a/zebra-state/src/service/read/address/balance.rs b/zebra-state/src/service/read/address/balance.rs index 14ec49fc..8c2b2107 100644 --- a/zebra-state/src/service/read/address/balance.rs +++ b/zebra-state/src/service/read/address/balance.rs @@ -26,116 +26,6 @@ use crate::{ 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>, - db: &ZebraDb, - addresses: HashSet, -) -> Result, 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, -) -> Result<(Amount, Option), 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, - addresses: &HashSet, - finalized_tip: Option, -) -> Amount { - // # 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, /// and return the result. fn apply_balance_change( diff --git a/zebra-state/src/service/read/address/tx_id.rs b/zebra-state/src/service/read/address/tx_id.rs index 27b9a9b3..ef599598 100644 --- a/zebra-state/src/service/read/address/tx_id.rs +++ b/zebra-state/src/service/read/address/tx_id.rs @@ -25,249 +25,6 @@ use crate::{ 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( - chain: Option, - db: &ZebraDb, - addresses: HashSet, - query_height_range: RangeInclusive, -) -> Result, BoxError> -where - C: AsRef, -{ - 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, - query_height_range: RangeInclusive, -) -> ( - BTreeMap, - Option>, -) { - // # 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( - chain: Option, - addresses: &HashSet, - finalized_tip_range: Option>, - query_height_range: RangeInclusive, -) -> Result, BoxError> -where - C: AsRef, -{ - 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. fn apply_tx_id_changes(