From b0249aa7f59e943509fc3ff4d73e9b068d1fdc96 Mon Sep 17 00:00:00 2001 From: Likho Date: Wed, 1 May 2024 18:08:33 +0200 Subject: [PATCH] WIP: Remove transparent transfers --- zebra-rpc/src/methods.rs | 225 -------------- zebra-rpc/src/methods/tests/prop.rs | 130 -------- zebra-rpc/src/methods/tests/snapshot.rs | 8 - ...validate_address_basic@mainnet_10.snap.new | 8 + zebra-state/src/request.rs | 10 +- zebra-state/src/response.rs | 7 +- zebra-state/src/service.rs | 26 -- .../src/service/non_finalized_state/chain.rs | 36 +-- .../non_finalized_state/chain/index.rs | 19 +- zebra-state/src/service/read.rs | 4 - .../src/service/read/address/balance.rs | 11 - zebra-state/src/service/read/address/tx_id.rs | 14 +- zebra-state/src/service/read/address/utxo.rs | 285 +----------------- 13 files changed, 26 insertions(+), 757 deletions(-) create mode 100644 zebra-rpc/src/methods/tests/snapshot/snapshots/validate_address_basic@mainnet_10.snap.new diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index d1dd0564..844892f4 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -87,34 +87,6 @@ pub trait Rpc { #[rpc(name = "getblockchaininfo")] fn get_blockchain_info(&self) -> Result; - /// Returns the total balance of a provided `addresses` in an [`AddressBalance`] instance. - /// - /// zcashd reference: [`getaddressbalance`](https://zcash.github.io/rpc/getaddressbalance.html) - /// method: post - /// tags: address - /// - /// # Parameters - /// - /// - `address_strings`: (object, example={"addresses": ["tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ"]}) A JSON map with a single entry - /// - `addresses`: (array of strings) A list of base-58 encoded addresses. - /// - /// # Notes - /// - /// zcashd also accepts a single string parameter instead of an array of strings, but Zebra - /// doesn't because lightwalletd always calls this RPC with an array of addresses. - /// - /// zcashd also returns the total amount of Zatoshis received by the addresses, but Zebra - /// doesn't because lightwalletd doesn't use that information. - /// - /// The RPC documentation says that the returned object has a string `balance` field, but - /// zcashd actually [returns an - /// integer](https://github.com/zcash/lightwalletd/blob/bdaac63f3ee0dbef62bde04f6817a9f90d483b00/common/common.go#L128-L130). - #[rpc(name = "getaddressbalance")] - fn get_address_balance( - &self, - address_strings: AddressStrings, - ) -> BoxFuture>; - /// Sends the raw bytes of a signed transaction to the local node's mempool, if the transaction is valid. /// Returns the [`SentTransactionHash`] for the transaction, as a JSON string. /// @@ -250,47 +222,6 @@ pub trait Rpc { txid_hex: String, verbose: Option, ) -> BoxFuture>; - - /// Returns the transaction ids made by the provided transparent addresses. - /// - /// zcashd reference: [`getaddresstxids`](https://zcash.github.io/rpc/getaddresstxids.html) - /// method: post - /// tags: address - /// - /// # Parameters - /// - /// - `request`: (object, required, example={\"addresses\": [\"tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ\"], \"start\": 1000, \"end\": 2000}) A struct with the following named fields: - /// - `addresses`: (json array of string, required) The addresses to get transactions from. - /// - `start`: (numeric, required) The lower height to start looking for transactions (inclusive). - /// - `end`: (numeric, required) The top height to stop looking for transactions (inclusive). - /// - /// # Notes - /// - /// Only the multi-argument format is used by lightwalletd and this is what we currently support: - /// - #[rpc(name = "getaddresstxids")] - fn get_address_tx_ids(&self, request: GetAddressTxIdsRequest) - -> BoxFuture>>; - - /// Returns all unspent outputs for a list of addresses. - /// - /// zcashd reference: [`getaddressutxos`](https://zcash.github.io/rpc/getaddressutxos.html) - /// method: post - /// tags: address - /// - /// # Parameters - /// - /// - `addresses`: (array, required, example={\"addresses\": [\"tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ\"]}) The addresses to get outputs from. - /// - /// # Notes - /// - /// lightwalletd always uses the multi-address request, without chaininfo: - /// - #[rpc(name = "getaddressutxos")] - fn get_address_utxos( - &self, - address_strings: AddressStrings, - ) -> BoxFuture>>; } /// RPC method implementations. @@ -595,33 +526,6 @@ where Ok(response) } - // TODO: use a generic error constructor (#5548) - fn get_address_balance( - &self, - address_strings: AddressStrings, - ) -> BoxFuture> { - let state = self.state.clone(); - - async move { - let valid_addresses = address_strings.valid_addresses()?; - - let request = zebra_state::ReadRequest::AddressBalance(valid_addresses); - let response = state.oneshot(request).await.map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; - - match response { - zebra_state::ReadResponse::AddressBalance(balance) => Ok(AddressBalance { - balance: u64::from(balance), - }), - _ => unreachable!("Unexpected response from state service: {response:?}"), - } - } - .boxed() - } - // TODO: use HexData or GetRawTransaction::Bytes to handle the transaction data argument // use a generic error constructor (#5548) fn send_raw_transaction( @@ -1279,135 +1183,6 @@ where } .boxed() } - - // TODO: use a generic error constructor (#5548) - fn get_address_tx_ids( - &self, - request: GetAddressTxIdsRequest, - ) -> BoxFuture>> { - let mut state = self.state.clone(); - let latest_chain_tip = self.latest_chain_tip.clone(); - - let start = Height(request.start); - let end = Height(request.end); - - async move { - let chain_height = best_chain_tip_height(&latest_chain_tip)?; - - // height range checks - check_height_range(start, end, chain_height)?; - - let valid_addresses = AddressStrings { - addresses: request.addresses, - } - .valid_addresses()?; - - let request = zebra_state::ReadRequest::TransactionIdsByAddresses { - addresses: valid_addresses, - height_range: start..=end, - }; - let response = state - .ready() - .and_then(|service| service.call(request)) - .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; - - let hashes = match response { - zebra_state::ReadResponse::AddressesTransactionIds(hashes) => { - let mut last_tx_location = TransactionLocation::from_usize(Height(0), 0); - - hashes - .iter() - .map(|(tx_loc, tx_id)| { - // Check that the returned transactions are in chain order. - assert!( - *tx_loc > last_tx_location, - "Transactions were not in chain order:\n\ - {tx_loc:?} {tx_id:?} was after:\n\ - {last_tx_location:?}", - ); - - last_tx_location = *tx_loc; - - tx_id.to_string() - }) - .collect() - } - _ => unreachable!("unmatched response to a TransactionsByAddresses request"), - }; - - Ok(hashes) - } - .boxed() - } - - // TODO: use a generic error constructor (#5548) - fn get_address_utxos( - &self, - address_strings: AddressStrings, - ) -> BoxFuture>> { - let mut state = self.state.clone(); - let mut response_utxos = vec![]; - - async move { - let valid_addresses = address_strings.valid_addresses()?; - - // get utxos data for addresses - let request = zebra_state::ReadRequest::UtxosByAddresses(valid_addresses); - let response = state - .ready() - .and_then(|service| service.call(request)) - .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; - let utxos = match response { - zebra_state::ReadResponse::AddressUtxos(utxos) => utxos, - _ => unreachable!("unmatched response to a UtxosByAddresses request"), - }; - - let mut last_output_location = OutputLocation::from_usize(Height(0), 0, 0); - - for utxo_data in utxos.utxos() { - let address = utxo_data.0; - let txid = *utxo_data.1; - let height = utxo_data.2.height(); - let output_index = utxo_data.2.output_index(); - let script = utxo_data.3.lock_script.clone(); - let satoshis = u64::from(utxo_data.3.value); - - let output_location = *utxo_data.2; - // Check that the returned UTXOs are in chain order. - assert!( - output_location > last_output_location, - "UTXOs were not in chain order:\n\ - {output_location:?} {address:?} {txid:?} was after:\n\ - {last_output_location:?}", - ); - - let entry = GetAddressUtxos { - address, - txid, - output_index, - script, - satoshis, - height, - }; - response_utxos.push(entry); - - last_output_location = output_location; - } - - Ok(response_utxos) - } - .boxed() - } } /// Returns the best chain tip height of `latest_chain_tip`, diff --git a/zebra-rpc/src/methods/tests/prop.rs b/zebra-rpc/src/methods/tests/prop.rs index 38c3c6b0..4c0ff713 100644 --- a/zebra-rpc/src/methods/tests/prop.rs +++ b/zebra-rpc/src/methods/tests/prop.rs @@ -652,136 +652,6 @@ proptest! { })?; } - /// Test the `get_address_balance` RPC using an arbitrary set of addresses. - #[test] - fn queries_balance_for_valid_addresses( - network in any::(), - addresses in any::>(), - balance in any::>(), - ) { - let (runtime, _init_guard) = zebra_test::init_async(); - let _guard = runtime.enter(); - - let mut mempool = MockService::build().for_prop_tests(); - let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests(); - - // Create a mocked `ChainTip` - let (chain_tip, _mock_chain_tip_sender) = MockChainTip::new(); - - // Prepare the list of addresses. - let address_strings = AddressStrings { - addresses: addresses - .iter() - .map(|address| address.to_string()) - .collect(), - }; - - tokio::time::pause(); - - // Start RPC with the mocked `ChainTip` - runtime.block_on(async move { - let (rpc, _rpc_tx_queue_task_handle) = RpcImpl::new( - "RPC test", - "RPC test", - network, - false, - true, - mempool.clone(), - Buffer::new(state.clone(), 1), - chain_tip, - ); - - // Build the future to call the RPC - let call = rpc.get_address_balance(address_strings); - - // The RPC should perform a state query - let state_query = state - .expect_request(zebra_state::ReadRequest::AddressBalance(addresses)) - .map_ok(|responder| { - responder.respond(zebra_state::ReadResponse::AddressBalance(balance)) - }); - - // Await the RPC call and the state query - let (response, state_query_result) = join!(call, state_query); - - state_query_result?; - - // Check that response contains the expected balance - let received_balance = response?; - - prop_assert_eq!(received_balance, AddressBalance { balance: balance.into() }); - - // Check no further requests were made during this test - mempool.expect_no_requests().await?; - state.expect_no_requests().await?; - - Ok::<_, TestCaseError>(()) - })?; - } - - /// Test the `get_address_balance` RPC using an invalid list of addresses. - /// - /// An error should be returned. - #[test] - fn does_not_query_balance_for_invalid_addresses( - network in any::(), - at_least_one_invalid_address in vec(".*", 1..10), - ) { - let (runtime, _init_guard) = zebra_test::init_async(); - let _guard = runtime.enter(); - - prop_assume!(at_least_one_invalid_address - .iter() - .any(|string| string.parse::().is_err())); - - let mut mempool = MockService::build().for_prop_tests(); - let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests(); - - // Create a mocked `ChainTip` - let (chain_tip, _mock_chain_tip_sender) = MockChainTip::new(); - - tokio::time::pause(); - - // Start RPC with the mocked `ChainTip` - runtime.block_on(async move { - let (rpc, _rpc_tx_queue_task_handle) = RpcImpl::new( - "RPC test", - "RPC test", - network, - false, - true, - mempool.clone(), - Buffer::new(state.clone(), 1), - chain_tip, - ); - - let address_strings = AddressStrings { - addresses: at_least_one_invalid_address, - }; - - // Build the future to call the RPC - let result = rpc.get_address_balance(address_strings).await; - - // Check that the invalid addresses lead to an error - prop_assert!( - matches!( - result, - Err(Error { - code: ErrorCode::InvalidParams, - .. - }) - ), - "Result is not a server error: {result:?}" - ); - - // Check no requests were made during this test - mempool.expect_no_requests().await?; - state.expect_no_requests().await?; - - Ok::<_, TestCaseError>(()) - })?; - } - /// Test the queue functionality using `send_raw_transaction` #[test] fn rpc_queue_main_loop(tx in any::()) { diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index b9788053..711bffe6 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -102,14 +102,6 @@ async fn test_rpc_response_data_for_network(network: &Network) { .unwrap(); let addresses = vec![address.to_string()]; - // `getaddressbalance` - let get_address_balance = rpc - .get_address_balance(AddressStrings { - addresses: addresses.clone(), - }) - .await - .expect("We should have an AddressBalance struct"); - snapshot_rpc_getaddressbalance(get_address_balance, &settings); // `getblock` variants // A valid block height in the populated state diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/validate_address_basic@mainnet_10.snap.new b/zebra-rpc/src/methods/tests/snapshot/snapshots/validate_address_basic@mainnet_10.snap.new new file mode 100644 index 00000000..0e267ce6 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/validate_address_basic@mainnet_10.snap.new @@ -0,0 +1,8 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +assertion_line: 584 +expression: validate_address +--- +{ + "isvalid": false +} diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 9adba765..875610e0 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -1,8 +1,8 @@ //! State [`tower::Service`] request types. use std::{ - collections::{HashMap, HashSet}, - ops::{Deref, DerefMut, RangeInclusive}, + collections::HashMap, + ops::{Deref, DerefMut}, sync::Arc, }; @@ -935,11 +935,6 @@ pub enum ReadRequest { limit: Option, }, - /// Looks up utxos for the provided addresses. - /// - /// Returns a type with found utxos and transaction information. - UtxosByAddresses(HashSet), - /// Contextually validates anchors and nullifiers of a transaction on the best chain /// /// Returns [`ReadResponse::ValidBestChainTipNullifiersAndAnchors`]. @@ -1007,7 +1002,6 @@ impl ReadRequest { ReadRequest::OrchardTree { .. } => "orchard_tree", ReadRequest::SaplingSubtrees { .. } => "sapling_subtrees", ReadRequest::OrchardSubtrees { .. } => "orchard_subtrees", - ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses", ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => { "best_chain_tip_nullifiers_anchors" } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 242191f8..9b2786df 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -20,7 +20,7 @@ use zebra_chain::work::difficulty::CompactDifficulty; #[allow(unused_imports)] use crate::{ReadRequest, Request}; -use crate::{service::read::AddressUtxos, TransactionLocation}; +use crate::{TransactionLocation}; #[derive(Clone, Debug, PartialEq, Eq)] /// A response to a [`StateService`](crate::service::StateService) [`Request`]. @@ -190,8 +190,6 @@ pub enum ReadResponse { /// with the obtained transaction ids, in the order they appear in blocks. AddressesTransactionIds(BTreeMap), - /// Response to [`ReadRequest::UtxosByAddresses`] with found utxos and transaction data. - AddressUtxos(AddressUtxos), /// Response to [`ReadRequest::CheckBestChainTipNullifiersAndAnchors`]. /// @@ -293,8 +291,7 @@ impl TryFrom for Response { | ReadResponse::SaplingSubtrees(_) | ReadResponse::OrchardSubtrees(_) | ReadResponse::AddressBalance(_) - | ReadResponse::AddressesTransactionIds(_) - | ReadResponse::AddressUtxos(_) => { + | ReadResponse::AddressesTransactionIds(_) => { Err("there is no corresponding Response for this ReadResponse") } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 6f35d5fa..04546fe6 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1616,32 +1616,6 @@ impl Service for ReadStateService { .wait_for_panics() } - // For the get_address_utxos RPC. - ReadRequest::UtxosByAddresses(addresses) => { - let state = self.clone(); - - tokio::task::spawn_blocking(move || { - span.in_scope(move || { - let utxos = state.non_finalized_state_receiver.with_watch_data( - |non_finalized_state| { - read::address_utxos( - &state.network, - non_finalized_state.best_chain(), - &state.db, - addresses, - ) - }, - ); - - // The work is done in the future. - timer.finish(module_path!(), line!(), "ReadRequest::UtxosByAddresses"); - - utxos.map(ReadResponse::AddressUtxos) - }) - }) - .wait_for_panics() - } - ReadRequest::CheckBestChainTipNullifiersAndAnchors(unmined_tx) => { 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 01cfd527..7024989f 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -3,8 +3,8 @@ use std::{ cmp::Ordering, - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, - ops::{Deref, DerefMut, RangeInclusive}, + collections::{BTreeMap, HashMap, HashSet}, + ops::{Deref, DerefMut}, sync::Arc, }; @@ -12,7 +12,7 @@ use mset::MultiSet; use tracing::instrument; use zebra_chain::{ - amount::{Amount, NegativeAllowed, NonNegative}, + amount::{NegativeAllowed, NonNegative}, block::{self, Height}, history_tree::HistoryTree, orchard, @@ -29,7 +29,7 @@ use zebra_chain::{ }; use crate::{ - request::Treestate, service::check, ContextuallyVerifiedBlock, HashOrHeight, OutputLocation, + request::Treestate, service::check, ContextuallyVerifiedBlock, HashOrHeight, TransactionLocation, ValidateContextError, }; @@ -1326,16 +1326,12 @@ impl Chain { block, hash, height, - new_outputs, - spent_outputs, transaction_hashes, chain_value_pool_change, ) = ( contextually_valid.block.as_ref(), contextually_valid.hash, contextually_valid.height, - &contextually_valid.new_outputs, - &contextually_valid.spent_outputs, &contextually_valid.transaction_hashes, &contextually_valid.chain_value_pool_change, ); @@ -1363,29 +1359,21 @@ impl Chain { .enumerate() { let ( - inputs, - outputs, joinsplit_data, sapling_shielded_data_per_spend_anchor, sapling_shielded_data_shared_anchor, orchard_shielded_data, ) = match transaction.deref() { V4 { - inputs, - outputs, joinsplit_data, sapling_shielded_data, .. - } => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None), + } => (joinsplit_data, sapling_shielded_data, &None, &None), V5 { - inputs, - outputs, sapling_shielded_data, orchard_shielded_data, .. } => ( - inputs, - outputs, &None, &None, sapling_shielded_data, @@ -1485,16 +1473,12 @@ impl UpdateWith for Chain { block, hash, height, - new_outputs, - spent_outputs, transaction_hashes, chain_value_pool_change, ) = ( contextually_valid.block.as_ref(), contextually_valid.hash, contextually_valid.height, - &contextually_valid.new_outputs, - &contextually_valid.spent_outputs, &contextually_valid.transaction_hashes, &contextually_valid.chain_value_pool_change, ); @@ -1519,29 +1503,21 @@ impl UpdateWith for Chain { block.transactions.iter().zip(transaction_hashes.iter()) { let ( - inputs, - outputs, joinsplit_data, sapling_shielded_data_per_spend_anchor, sapling_shielded_data_shared_anchor, orchard_shielded_data, ) = match transaction.deref() { V4 { - inputs, - outputs, joinsplit_data, sapling_shielded_data, .. - } => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None), + } => (joinsplit_data, sapling_shielded_data, &None, &None), V5 { - inputs, - outputs, sapling_shielded_data, orchard_shielded_data, .. } => ( - inputs, - outputs, &None, &None, sapling_shielded_data, 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 2852ff5e..b8ecb65b 100644 --- a/zebra-state/src/service/non_finalized_state/chain/index.rs +++ b/zebra-state/src/service/non_finalized_state/chain/index.rs @@ -1,23 +1,10 @@ //! Transparent address indexes for non-finalized chains. -use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, - ops::RangeInclusive, -}; +use zebra_chain::transparent; -use mset::MultiSet; - -use zebra_chain::{ - amount::{Amount, NegativeAllowed}, - block::Height, - transaction, transparent, -}; - -use crate::{OutputLocation, TransactionLocation, ValidateContextError}; - -use super::{RevertPosition, UpdateWith}; +use crate::TransactionLocation; /// 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) + TransactionLocation::from_usize(ordered_utxo.utxo.height, ordered_utxo.clone().tx_index_in_block) } diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index 824740ce..d50583fe 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -24,10 +24,6 @@ pub mod difficulty; #[cfg(test)] mod tests; - -pub use address::{ - utxo::{address_utxos, AddressUtxos}, -}; pub use block::{ any_utxo, block, block_header, mined_transaction, transaction_hashes_for_block, unspent_utxo, }; diff --git a/zebra-state/src/service/read/address/balance.rs b/zebra-state/src/service/read/address/balance.rs index 8c2b2107..934fbd97 100644 --- a/zebra-state/src/service/read/address/balance.rs +++ b/zebra-state/src/service/read/address/balance.rs @@ -11,19 +11,8 @@ //! - the cached [`Chain`], and //! - the shared finalized [`ZebraDb`] reference. -use std::{collections::HashSet, sync::Arc}; - use zebra_chain::{ amount::{self, Amount, NegativeAllowed, NonNegative}, - block::Height, - transparent, -}; - -use crate::{ - service::{ - finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES, - }, - BoxError, }; /// Add the supplied finalized and non-finalized balances together, diff --git a/zebra-state/src/service/read/address/tx_id.rs b/zebra-state/src/service/read/address/tx_id.rs index ef599598..e4cbaf0c 100644 --- a/zebra-state/src/service/read/address/tx_id.rs +++ b/zebra-state/src/service/read/address/tx_id.rs @@ -11,19 +11,11 @@ //! - the cached [`Chain`], and //! - the shared finalized [`ZebraDb`] reference. -use std::{ - collections::{BTreeMap, HashSet}, - ops::RangeInclusive, -}; +use std::collections::BTreeMap; -use zebra_chain::{block::Height, transaction, transparent}; +use zebra_chain::transaction; -use crate::{ - service::{ - finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES, - }, - BoxError, TransactionLocation, -}; +use crate::TransactionLocation; /// Returns the combined finalized and non-finalized transaction IDs. diff --git a/zebra-state/src/service/read/address/utxo.rs b/zebra-state/src/service/read/address/utxo.rs index d045bd4d..45c7866a 100644 --- a/zebra-state/src/service/read/address/utxo.rs +++ b/zebra-state/src/service/read/address/utxo.rs @@ -19,10 +19,8 @@ use std::{ use zebra_chain::{block::Height, parameters::Network, transaction, transparent}; use crate::{ - service::{ - finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES, - }, - BoxError, OutputLocation, TransactionLocation, + service::finalized_state::ZebraDb, + OutputLocation, TransactionLocation, }; /// The full range of address heights. @@ -89,86 +87,6 @@ impl AddressUtxos { } } -/// Returns the unspent transparent outputs (UTXOs) for the supplied [`transparent::Address`]es, -/// in chain order; and the transaction IDs for the transactions containing those UTXOs. -/// -/// If the addresses do not exist in the non-finalized `chain` or finalized `db`, -/// returns an empty list. -pub fn address_utxos( - network: &Network, - chain: Option, - db: &ZebraDb, - addresses: HashSet, -) -> Result -where - C: AsRef, -{ - let mut utxo_error = None; - let address_count = addresses.len(); - - // Retry the finalized UTXO 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 attempt in 0..=FINALIZED_STATE_QUERY_RETRIES { - debug!(?attempt, ?address_count, "starting address UTXO query"); - - let (finalized_utxos, finalized_tip_range) = finalized_address_utxos(db, &addresses); - - debug!( - finalized_utxo_count = ?finalized_utxos.len(), - ?finalized_tip_range, - ?address_count, - ?attempt, - "finalized address UTXO response", - ); - - // Apply the non-finalized UTXO changes. - let chain_utxo_changes = - chain_transparent_utxo_changes(chain.as_ref(), &addresses, finalized_tip_range); - - // If the UTXOs are valid, return them, otherwise, retry or return an error. - match chain_utxo_changes { - Ok((created_chain_utxos, spent_chain_utxos)) => { - debug!( - chain_utxo_count = ?created_chain_utxos.len(), - chain_utxo_spent = ?spent_chain_utxos.len(), - ?address_count, - ?attempt, - "chain address UTXO response", - ); - - let utxos = - apply_utxo_changes(finalized_utxos, created_chain_utxos, spent_chain_utxos); - let tx_ids = lookup_tx_ids_for_utxos(chain, db, &addresses, &utxos); - - debug!( - full_utxo_count = ?utxos.len(), - tx_id_count = ?tx_ids.len(), - ?address_count, - ?attempt, - "full address UTXO response", - ); - - return Ok(AddressUtxos::new(network, utxos, tx_ids)); - } - - Err(chain_utxo_error) => { - debug!( - ?chain_utxo_error, - ?address_count, - ?attempt, - "chain address UTXO response", - ); - - utxo_error = Some(Err(chain_utxo_error)) - } - } - } - - utxo_error.expect("unexpected missing error: attempts should set error or return") -} - /// Returns the unspent transparent outputs (UTXOs) for `addresses` in the finalized chain, /// and the finalized tip heights the UTXOs were queried at. /// @@ -205,160 +123,6 @@ fn finalized_address_utxos( (finalized_utxos, finalized_tip_range) } -/// Returns the UTXO changes for `addresses` in the non-finalized chain, -/// matching or overlapping the UTXOs 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_utxo_changes( - chain: Option, - addresses: &HashSet, - finalized_tip_range: Option>, -) -> Result< - ( - BTreeMap, - BTreeSet, - ), - 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 UTXO query: state is empty, no UTXOs available", - ); - - return Ok(Default::default()); - } - }; - - // # Correctness - // - // We can compensate for deleted UTXOs by applying the overlapping non-finalized UTXO changes. - - // 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 finalized_tip_status.is_ok() { - debug!( - ?finalized_tip_status, - ?required_min_non_finalized_root, - ?finalized_tip_range, - ?address_count, - "chain address UTXO query: \ - finalized chain is consistent, and non-finalized chain is empty", - ); - - return Ok(Default::default()); - } 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 UTXO query: \ - finalized tip query was inconsistent, but non-finalized chain is empty", - ); - - return Err("unable to get UTXOs: \ - state was committing a block, and non-finalized chain is empty" - .into()); - } - } - - let chain = chain.unwrap(); - let chain = chain.as_ref(); - - let non_finalized_root = chain.non_finalized_root_height(); - let non_finalized_tip = chain.non_finalized_tip_height(); - - assert!( - non_finalized_root.0 <= required_min_non_finalized_root, - "unexpected chain gap: the best chain is updated after its previous root is finalized", - ); - - match finalized_tip_status { - Ok(finalized_tip_height) => { - // If 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 UTXO 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 *required_non_finalized_overlap.end() > non_finalized_tip.0 { - debug!( - ?non_finalized_root, - ?non_finalized_tip, - ?finalized_tip_status, - ?finalized_tip_range, - ?address_count, - "chain address UTXO query: \ - finalized tip query was inconsistent, \ - and some inconsistent blocks are missing from the non-finalized chain", - ); - - return Err("unable to get UTXOs: \ - state was committing a block, \ - that is missing from the non-finalized chain" - .into()); - } - - // 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!( - required_non_finalized_overlap - .clone() - .all(|height| chain.blocks.contains_key(&Height(height))), - "UTXO query inconsistency: chain must contain required overlap blocks", - ); - } - } - - Ok(chain.partial_transparent_utxo_changes(addresses)) -} - /// Combines the supplied finalized and non-finalized UTXOs, /// removes the spent UTXOs, and returns the result. fn apply_utxo_changes( @@ -374,48 +138,3 @@ fn apply_utxo_changes( .filter(|(utxo_location, _output)| !spent_chain_utxos.contains(utxo_location)) .collect() } - -/// Returns the [`transaction::Hash`]es containing the supplied UTXOs, -/// from the non-finalized `chain` and finalized `db`. -/// -/// # Panics -/// -/// If any UTXO is not in the supplied state. -fn lookup_tx_ids_for_utxos( - chain: Option, - db: &ZebraDb, - addresses: &HashSet, - utxos: &BTreeMap, -) -> BTreeMap -where - C: AsRef, -{ - // Get the unique set of transaction locations - let transaction_locations: BTreeSet = utxos - .keys() - .map(|output_location| output_location.transaction_location()) - .collect(); - - let chain_tx_ids = chain - .as_ref() - .map(|chain| { - chain - .as_ref() - .partial_transparent_tx_ids(addresses, ADDRESS_HEIGHTS_FULL_RANGE) - }) - .unwrap_or_default(); - - // First try the in-memory chain, then the disk database - transaction_locations - .iter() - .map(|tx_loc| { - ( - *tx_loc, - chain_tx_ids.get(tx_loc).cloned().unwrap_or_else(|| { - db.transaction_hash(*tx_loc) - .expect("unexpected inconsistent UTXO indexes") - }), - ) - }) - .collect() -}