WIP: Remove transparent transfers
This commit is contained in:
parent
92f2966e25
commit
b0249aa7f5
|
|
@ -87,34 +87,6 @@ pub trait Rpc {
|
|||
#[rpc(name = "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.
|
||||
/// Returns the [`SentTransactionHash`] for the transaction, as a JSON string.
|
||||
///
|
||||
|
|
@ -250,47 +222,6 @@ pub trait Rpc {
|
|||
txid_hex: String,
|
||||
verbose: Option<u8>,
|
||||
) -> 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.
|
||||
|
|
@ -595,33 +526,6 @@ where
|
|||
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
|
||||
// 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<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`,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
fn rpc_queue_main_loop(tx in any::<Transaction>()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
|
||||
assertion_line: 584
|
||||
expression: validate_address
|
||||
---
|
||||
{
|
||||
"isvalid": false
|
||||
}
|
||||
|
|
@ -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<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
|
||||
///
|
||||
/// 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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TransactionLocation, transaction::Hash>),
|
||||
|
||||
/// Response to [`ReadRequest::UtxosByAddresses`] with found utxos and transaction data.
|
||||
AddressUtxos(AddressUtxos),
|
||||
|
||||
/// Response to [`ReadRequest::CheckBestChainTipNullifiersAndAnchors`].
|
||||
///
|
||||
|
|
@ -293,8 +291,7 @@ impl TryFrom<ReadResponse> for Response {
|
|||
| ReadResponse::SaplingSubtrees(_)
|
||||
| ReadResponse::OrchardSubtrees(_)
|
||||
| ReadResponse::AddressBalance(_)
|
||||
| ReadResponse::AddressesTransactionIds(_)
|
||||
| ReadResponse::AddressUtxos(_) => {
|
||||
| ReadResponse::AddressesTransactionIds(_) => {
|
||||
Err("there is no corresponding Response for this ReadResponse")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1616,32 +1616,6 @@ impl Service<ReadRequest> 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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ContextuallyVerifiedBlock> 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<ContextuallyVerifiedBlock> 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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<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,
|
||||
/// 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<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,
|
||||
/// 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<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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue