WIP: Remove transparent transfers

This commit is contained in:
Likho 2024-05-01 18:08:33 +02:00
parent 92f2966e25
commit b0249aa7f5
13 changed files with 26 additions and 757 deletions

View File

@ -87,34 +87,6 @@ pub trait Rpc {
#[rpc(name = "getblockchaininfo")] #[rpc(name = "getblockchaininfo")]
fn get_blockchain_info(&self) -> Result<GetBlockChainInfo>; fn get_blockchain_info(&self) -> Result<GetBlockChainInfo>;
/// 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<Result<AddressBalance>>;
/// Sends the raw bytes of a signed transaction to the local node's mempool, if the transaction is valid. /// 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. /// Returns the [`SentTransactionHash`] for the transaction, as a JSON string.
/// ///
@ -250,47 +222,6 @@ pub trait Rpc {
txid_hex: String, txid_hex: String,
verbose: Option<u8>, verbose: Option<u8>,
) -> BoxFuture<Result<GetRawTransaction>>; ) -> BoxFuture<Result<GetRawTransaction>>;
/// 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:
/// <https://github.com/zcash/lightwalletd/blob/631bb16404e3d8b045e74a7c5489db626790b2f6/common/common.go#L97-L102>
#[rpc(name = "getaddresstxids")]
fn get_address_tx_ids(&self, request: GetAddressTxIdsRequest)
-> BoxFuture<Result<Vec<String>>>;
/// 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:
/// <https://github.com/zcash/lightwalletd/blob/master/frontend/service.go#L402>
#[rpc(name = "getaddressutxos")]
fn get_address_utxos(
&self,
address_strings: AddressStrings,
) -> BoxFuture<Result<Vec<GetAddressUtxos>>>;
} }
/// RPC method implementations. /// RPC method implementations.
@ -595,33 +526,6 @@ where
Ok(response) Ok(response)
} }
// TODO: use a generic error constructor (#5548)
fn get_address_balance(
&self,
address_strings: AddressStrings,
) -> BoxFuture<Result<AddressBalance>> {
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 // TODO: use HexData or GetRawTransaction::Bytes to handle the transaction data argument
// use a generic error constructor (#5548) // use a generic error constructor (#5548)
fn send_raw_transaction( fn send_raw_transaction(
@ -1279,135 +1183,6 @@ where
} }
.boxed() .boxed()
} }
// TODO: use a generic error constructor (#5548)
fn get_address_tx_ids(
&self,
request: GetAddressTxIdsRequest,
) -> BoxFuture<Result<Vec<String>>> {
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<Result<Vec<GetAddressUtxos>>> {
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`, /// Returns the best chain tip height of `latest_chain_tip`,

View File

@ -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::<Network>(),
addresses in any::<HashSet<transparent::Address>>(),
balance in any::<Amount<NonNegative>>(),
) {
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::<Network>(),
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::<transparent::Address>().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 the queue functionality using `send_raw_transaction`
#[test] #[test]
fn rpc_queue_main_loop(tx in any::<Transaction>()) { fn rpc_queue_main_loop(tx in any::<Transaction>()) {

View File

@ -102,14 +102,6 @@ async fn test_rpc_response_data_for_network(network: &Network) {
.unwrap(); .unwrap();
let addresses = vec![address.to_string()]; 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 // `getblock` variants
// A valid block height in the populated state // A valid block height in the populated state

View File

@ -0,0 +1,8 @@
---
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
assertion_line: 584
expression: validate_address
---
{
"isvalid": false
}

View File

@ -1,8 +1,8 @@
//! State [`tower::Service`] request types. //! State [`tower::Service`] request types.
use std::{ use std::{
collections::{HashMap, HashSet}, collections::HashMap,
ops::{Deref, DerefMut, RangeInclusive}, ops::{Deref, DerefMut},
sync::Arc, sync::Arc,
}; };
@ -935,11 +935,6 @@ pub enum ReadRequest {
limit: Option<NoteCommitmentSubtreeIndex>, limit: Option<NoteCommitmentSubtreeIndex>,
}, },
/// Looks up utxos for the provided addresses.
///
/// Returns a type with found utxos and transaction information.
UtxosByAddresses(HashSet<transparent::Address>),
/// Contextually validates anchors and nullifiers of a transaction on the best chain /// Contextually validates anchors and nullifiers of a transaction on the best chain
/// ///
/// Returns [`ReadResponse::ValidBestChainTipNullifiersAndAnchors`]. /// Returns [`ReadResponse::ValidBestChainTipNullifiersAndAnchors`].
@ -1007,7 +1002,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::UtxosByAddresses(_) => "utxos_by_addesses",
ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => { ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => {
"best_chain_tip_nullifiers_anchors" "best_chain_tip_nullifiers_anchors"
} }

View File

@ -20,7 +20,7 @@ use zebra_chain::work::difficulty::CompactDifficulty;
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::{ReadRequest, Request}; use crate::{ReadRequest, Request};
use crate::{service::read::AddressUtxos, TransactionLocation}; use crate::{TransactionLocation};
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
/// A response to a [`StateService`](crate::service::StateService) [`Request`]. /// 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. /// with the obtained transaction ids, in the order they appear in blocks.
AddressesTransactionIds(BTreeMap<TransactionLocation, transaction::Hash>), AddressesTransactionIds(BTreeMap<TransactionLocation, transaction::Hash>),
/// Response to [`ReadRequest::UtxosByAddresses`] with found utxos and transaction data.
AddressUtxos(AddressUtxos),
/// Response to [`ReadRequest::CheckBestChainTipNullifiersAndAnchors`]. /// Response to [`ReadRequest::CheckBestChainTipNullifiersAndAnchors`].
/// ///
@ -293,8 +291,7 @@ impl TryFrom<ReadResponse> for Response {
| ReadResponse::SaplingSubtrees(_) | ReadResponse::SaplingSubtrees(_)
| ReadResponse::OrchardSubtrees(_) | ReadResponse::OrchardSubtrees(_)
| ReadResponse::AddressBalance(_) | ReadResponse::AddressBalance(_)
| ReadResponse::AddressesTransactionIds(_) | ReadResponse::AddressesTransactionIds(_) => {
| ReadResponse::AddressUtxos(_) => {
Err("there is no corresponding Response for this ReadResponse") Err("there is no corresponding Response for this ReadResponse")
} }

View File

@ -1616,32 +1616,6 @@ impl Service<ReadRequest> for ReadStateService {
.wait_for_panics() .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) => { ReadRequest::CheckBestChainTipNullifiersAndAnchors(unmined_tx) => {
let state = self.clone(); let state = self.clone();

View File

@ -3,8 +3,8 @@
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::{BTreeMap, BTreeSet, HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
ops::{Deref, DerefMut, RangeInclusive}, ops::{Deref, DerefMut},
sync::Arc, sync::Arc,
}; };
@ -12,7 +12,7 @@ use mset::MultiSet;
use tracing::instrument; use tracing::instrument;
use zebra_chain::{ use zebra_chain::{
amount::{Amount, NegativeAllowed, NonNegative}, amount::{NegativeAllowed, NonNegative},
block::{self, Height}, block::{self, Height},
history_tree::HistoryTree, history_tree::HistoryTree,
orchard, orchard,
@ -29,7 +29,7 @@ use zebra_chain::{
}; };
use crate::{ use crate::{
request::Treestate, service::check, ContextuallyVerifiedBlock, HashOrHeight, OutputLocation, request::Treestate, service::check, ContextuallyVerifiedBlock, HashOrHeight,
TransactionLocation, ValidateContextError, TransactionLocation, ValidateContextError,
}; };
@ -1326,16 +1326,12 @@ impl Chain {
block, block,
hash, hash,
height, height,
new_outputs,
spent_outputs,
transaction_hashes, transaction_hashes,
chain_value_pool_change, chain_value_pool_change,
) = ( ) = (
contextually_valid.block.as_ref(), contextually_valid.block.as_ref(),
contextually_valid.hash, contextually_valid.hash,
contextually_valid.height, contextually_valid.height,
&contextually_valid.new_outputs,
&contextually_valid.spent_outputs,
&contextually_valid.transaction_hashes, &contextually_valid.transaction_hashes,
&contextually_valid.chain_value_pool_change, &contextually_valid.chain_value_pool_change,
); );
@ -1363,29 +1359,21 @@ impl Chain {
.enumerate() .enumerate()
{ {
let ( let (
inputs,
outputs,
joinsplit_data, joinsplit_data,
sapling_shielded_data_per_spend_anchor, sapling_shielded_data_per_spend_anchor,
sapling_shielded_data_shared_anchor, sapling_shielded_data_shared_anchor,
orchard_shielded_data, orchard_shielded_data,
) = match transaction.deref() { ) = match transaction.deref() {
V4 { V4 {
inputs,
outputs,
joinsplit_data, joinsplit_data,
sapling_shielded_data, sapling_shielded_data,
.. ..
} => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None), } => (joinsplit_data, sapling_shielded_data, &None, &None),
V5 { V5 {
inputs,
outputs,
sapling_shielded_data, sapling_shielded_data,
orchard_shielded_data, orchard_shielded_data,
.. ..
} => ( } => (
inputs,
outputs,
&None, &None,
&None, &None,
sapling_shielded_data, sapling_shielded_data,
@ -1485,16 +1473,12 @@ impl UpdateWith<ContextuallyVerifiedBlock> for Chain {
block, block,
hash, hash,
height, height,
new_outputs,
spent_outputs,
transaction_hashes, transaction_hashes,
chain_value_pool_change, chain_value_pool_change,
) = ( ) = (
contextually_valid.block.as_ref(), contextually_valid.block.as_ref(),
contextually_valid.hash, contextually_valid.hash,
contextually_valid.height, contextually_valid.height,
&contextually_valid.new_outputs,
&contextually_valid.spent_outputs,
&contextually_valid.transaction_hashes, &contextually_valid.transaction_hashes,
&contextually_valid.chain_value_pool_change, &contextually_valid.chain_value_pool_change,
); );
@ -1519,29 +1503,21 @@ impl UpdateWith<ContextuallyVerifiedBlock> for Chain {
block.transactions.iter().zip(transaction_hashes.iter()) block.transactions.iter().zip(transaction_hashes.iter())
{ {
let ( let (
inputs,
outputs,
joinsplit_data, joinsplit_data,
sapling_shielded_data_per_spend_anchor, sapling_shielded_data_per_spend_anchor,
sapling_shielded_data_shared_anchor, sapling_shielded_data_shared_anchor,
orchard_shielded_data, orchard_shielded_data,
) = match transaction.deref() { ) = match transaction.deref() {
V4 { V4 {
inputs,
outputs,
joinsplit_data, joinsplit_data,
sapling_shielded_data, sapling_shielded_data,
.. ..
} => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None), } => (joinsplit_data, sapling_shielded_data, &None, &None),
V5 { V5 {
inputs,
outputs,
sapling_shielded_data, sapling_shielded_data,
orchard_shielded_data, orchard_shielded_data,
.. ..
} => ( } => (
inputs,
outputs,
&None, &None,
&None, &None,
sapling_shielded_data, sapling_shielded_data,

View File

@ -1,23 +1,10 @@
//! Transparent address indexes for non-finalized chains. //! Transparent address indexes for non-finalized chains.
use std::{ use zebra_chain::transparent;
collections::{BTreeMap, BTreeSet, HashMap},
ops::RangeInclusive,
};
use mset::MultiSet; use crate::TransactionLocation;
use zebra_chain::{
amount::{Amount, NegativeAllowed},
block::Height,
transaction, transparent,
};
use crate::{OutputLocation, TransactionLocation, ValidateContextError};
use super::{RevertPosition, UpdateWith};
/// 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.clone().tx_index_in_block)
} }

View File

@ -24,10 +24,6 @@ pub mod difficulty;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub use address::{
utxo::{address_utxos, AddressUtxos},
};
pub use block::{ pub use block::{
any_utxo, block, block_header, mined_transaction, transaction_hashes_for_block, unspent_utxo, any_utxo, block, block_header, mined_transaction, transaction_hashes_for_block, unspent_utxo,
}; };

View File

@ -11,19 +11,8 @@
//! - the cached [`Chain`], and //! - the cached [`Chain`], and
//! - the shared finalized [`ZebraDb`] reference. //! - the shared finalized [`ZebraDb`] reference.
use std::{collections::HashSet, sync::Arc};
use zebra_chain::{ use zebra_chain::{
amount::{self, Amount, NegativeAllowed, NonNegative}, 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, /// Add the supplied finalized and non-finalized balances together,

View File

@ -11,19 +11,11 @@
//! - the cached [`Chain`], and //! - the cached [`Chain`], and
//! - the shared finalized [`ZebraDb`] reference. //! - the shared finalized [`ZebraDb`] reference.
use std::{ use std::collections::BTreeMap;
collections::{BTreeMap, HashSet},
ops::RangeInclusive,
};
use zebra_chain::{block::Height, transaction, transparent}; use zebra_chain::transaction;
use crate::{ use crate::TransactionLocation;
service::{
finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES,
},
BoxError, TransactionLocation,
};
/// Returns the combined finalized and non-finalized transaction IDs. /// Returns the combined finalized and non-finalized transaction IDs.

View File

@ -19,10 +19,8 @@ use std::{
use zebra_chain::{block::Height, parameters::Network, transaction, transparent}; use zebra_chain::{block::Height, parameters::Network, transaction, transparent};
use crate::{ use crate::{
service::{ service::finalized_state::ZebraDb,
finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES, OutputLocation, TransactionLocation,
},
BoxError, OutputLocation, TransactionLocation,
}; };
/// The full range of address heights. /// 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<C>(
network: &Network,
chain: Option<C>,
db: &ZebraDb,
addresses: HashSet<transparent::Address>,
) -> Result<AddressUtxos, BoxError>
where
C: AsRef<Chain>,
{
let mut utxo_error = None;
let address_count = addresses.len();
// 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, /// Returns the unspent transparent outputs (UTXOs) for `addresses` in the finalized chain,
/// and the finalized tip heights the UTXOs were queried at. /// and the finalized tip heights the UTXOs were queried at.
/// ///
@ -205,160 +123,6 @@ fn finalized_address_utxos(
(finalized_utxos, finalized_tip_range) (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<C>(
chain: Option<C>,
addresses: &HashSet<transparent::Address>,
finalized_tip_range: Option<RangeInclusive<Height>>,
) -> Result<
(
BTreeMap<OutputLocation, transparent::Output>,
BTreeSet<OutputLocation>,
),
BoxError,
>
where
C: AsRef<Chain>,
{
let address_count = addresses.len();
let finalized_tip_range = match finalized_tip_range {
Some(finalized_tip_range) => finalized_tip_range,
None => {
assert!(
chain.is_none(),
"unexpected non-finalized chain when finalized state is empty"
);
debug!(
?finalized_tip_range,
?address_count,
"chain address UTXO query: state is empty, no UTXOs available",
);
return Ok(Default::default());
}
};
// # 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, /// Combines the supplied finalized and non-finalized UTXOs,
/// removes the spent UTXOs, and returns the result. /// removes the spent UTXOs, and returns the result.
fn apply_utxo_changes( fn apply_utxo_changes(
@ -374,48 +138,3 @@ fn apply_utxo_changes(
.filter(|(utxo_location, _output)| !spent_chain_utxos.contains(utxo_location)) .filter(|(utxo_location, _output)| !spent_chain_utxos.contains(utxo_location))
.collect() .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<C>(
chain: Option<C>,
db: &ZebraDb,
addresses: &HashSet<transparent::Address>,
utxos: &BTreeMap<OutputLocation, transparent::Output>,
) -> BTreeMap<TransactionLocation, transaction::Hash>
where
C: AsRef<Chain>,
{
// Get the unique set of transaction locations
let transaction_locations: BTreeSet<TransactionLocation> = utxos
.keys()
.map(|output_location| output_location.transaction_location())
.collect();
let chain_tx_ids = chain
.as_ref()
.map(|chain| {
chain
.as_ref()
.partial_transparent_tx_ids(addresses, ADDRESS_HEIGHTS_FULL_RANGE)
})
.unwrap_or_default();
// 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()
}