feat(rpc): Implement `getaddressutxos` RPC method. (#4087)

* implement display for `Script`

* implement `getaddressutxos`

* fix space

* normalize list of addresses as argument to rpc methods

* implement `AddressStrings`

* make a doc clearer

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Alfredo Garcia 2022-04-25 00:00:52 -03:00 committed by GitHub
parent e90c22917d
commit 56aabb1db1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 261 additions and 30 deletions

View File

@ -2,6 +2,8 @@
use std::{fmt, io};
use hex::ToHex;
use crate::serialization::{
zcash_serialize_bytes, SerializationError, ZcashDeserialize, ZcashSerialize,
};
@ -40,6 +42,12 @@ impl Script {
}
}
impl fmt::Display for Script {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.encode_hex::<String>())
}
}
impl fmt::Debug for Script {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("Script")
@ -48,6 +56,26 @@ impl fmt::Debug for Script {
}
}
impl ToHex for &Script {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
self.as_raw_bytes().encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
self.as_raw_bytes().encode_hex_upper()
}
}
impl ToHex for Script {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex_upper()
}
}
impl ZcashSerialize for Script {
fn zcash_serialize<W: io::Write>(&self, writer: W) -> Result<(), io::Error> {
zcash_serialize_bytes(&self.0, writer)

View File

@ -190,10 +190,28 @@ pub trait Rpc {
#[rpc(name = "getaddresstxids")]
fn get_address_tx_ids(
&self,
addresses: Vec<String>,
address_strings: AddressStrings,
start: u32,
end: u32,
) -> BoxFuture<Result<Vec<String>>>;
/// Returns all unspent outputs for a list of addresses.
///
/// zcashd reference: [`getaddressutxos`](https://zcash.github.io/rpc/getaddressutxos.html)
///
/// # Parameters
///
/// - `addresses`: (json array of string, required) 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.
@ -403,17 +421,9 @@ where
let state = self.state.clone();
async move {
let addresses: HashSet<Address> = address_strings
.addresses
.into_iter()
.map(|address| {
address.parse().map_err(|error| {
Error::invalid_params(&format!("invalid address {address:?}: {error}"))
})
})
.collect::<Result<_>>()?;
let valid_addresses = address_strings.valid_addresses()?;
let request = zebra_state::ReadRequest::AddressBalance(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(),
@ -642,7 +652,7 @@ where
fn get_address_tx_ids(
&self,
addresses: Vec<String>,
address_strings: AddressStrings,
start: u32,
end: u32,
) -> BoxFuture<Result<Vec<String>>> {
@ -660,17 +670,10 @@ where
// height range checks
check_height_range(start, end, chain_height?)?;
let valid_addresses: Result<HashSet<Address>> = addresses
.iter()
.map(|address| {
address.parse().map_err(|_| {
Error::invalid_params(format!("Provided address is not valid: {}", address))
})
})
.collect();
let valid_addresses = address_strings.valid_addresses()?;
let request = zebra_state::ReadRequest::TransactionIdsByAddresses {
addresses: valid_addresses?,
addresses: valid_addresses,
height_range: start..=end,
};
let response = state
@ -694,6 +697,56 @@ where
}
.boxed()
}
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::Utxos(utxos) => utxos,
_ => unreachable!("unmatched response to a UtxosByAddresses request"),
};
for utxo_data in utxos.utxos() {
let address = utxo_data.0.to_string();
let txid = utxo_data.1.to_string();
let height = utxo_data.2.height().0;
let output_index = utxo_data.2.output_index().as_usize();
let script = utxo_data.3.lock_script.to_string();
let satoshis = i64::from(utxo_data.3.value);
let entry = GetAddressUtxos {
address,
txid,
height,
output_index,
script,
satoshis,
};
response_utxos.push(entry);
}
Ok(response_utxos)
}
.boxed()
}
}
/// Response to a `getinfo` RPC request.
@ -722,12 +775,37 @@ pub struct GetBlockChainInfo {
/// A wrapper type with a list of strings of addresses.
///
/// This is used for the input parameter of [`Rpc::get_account_balance`].
/// This is used for the input parameter of [`Rpc::get_address_balance`],
/// [`Rpc::get_address_tx_ids`] and [`Rpc::get_address_utxos`].
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize)]
pub struct AddressStrings {
addresses: Vec<String>,
}
impl AddressStrings {
// Creates a new `AddressStrings` given a vector.
#[cfg(test)]
pub fn new(addresses: Vec<String>) -> AddressStrings {
AddressStrings { addresses }
}
/// Given a list of addresses as strings:
/// - check if provided list have all valid transparent addresses.
/// - return valid addresses as a set of `Address`.
pub fn valid_addresses(self) -> Result<HashSet<Address>> {
let valid_addresses: HashSet<Address> = self
.addresses
.into_iter()
.map(|address| {
address.parse().map_err(|error| {
Error::invalid_params(&format!("invalid address {address:?}: {error}"))
})
})
.collect::<Result<_>>()?;
Ok(valid_addresses)
}
}
/// The transparent balance of a set of addresses.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, serde::Serialize)]
pub struct AddressBalance {
@ -810,6 +888,20 @@ pub enum GetRawTransaction {
},
}
/// Response to a `getaddressutxos` RPC request.
///
/// See the notes for the [`Rpc::get_address_utxos` method].
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct GetAddressUtxos {
address: String,
txid: String,
height: u32,
#[serde(rename = "outputIndex")]
output_index: usize,
script: String,
satoshis: i64,
}
impl GetRawTransaction {
fn from_transaction(
tx: Arc<Transaction>,

View File

@ -337,12 +337,15 @@ async fn rpc_getaddresstxids_invalid_arguments() {
let start: u32 = 1;
let end: u32 = 2;
let error = rpc
.get_address_tx_ids(addresses, start, end)
.get_address_tx_ids(AddressStrings::new(addresses), start, end)
.await
.unwrap_err();
assert_eq!(
error.message,
format!("Provided address is not valid: {}", address)
format!(
"invalid address \"{}\": parse error: t-addr decoding error",
address
)
);
// create a valid address
@ -353,7 +356,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
let start: u32 = 2;
let end: u32 = 1;
let error = rpc
.get_address_tx_ids(addresses.clone(), start, end)
.get_address_tx_ids(AddressStrings::new(addresses.clone()), start, end)
.await
.unwrap_err();
assert_eq!(
@ -365,7 +368,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
let start: u32 = 0;
let end: u32 = 1;
let error = rpc
.get_address_tx_ids(addresses.clone(), start, end)
.get_address_tx_ids(AddressStrings::new(addresses.clone()), start, end)
.await
.unwrap_err();
assert_eq!(
@ -377,7 +380,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
let start: u32 = 1;
let end: u32 = 11;
let error = rpc
.get_address_tx_ids(addresses, start, end)
.get_address_tx_ids(AddressStrings::new(addresses), start, end)
.await
.unwrap_err();
assert_eq!(
@ -456,7 +459,7 @@ async fn rpc_getaddresstxids_response_with(
// call the method with valid arguments
let addresses = vec![address.to_string()];
let response = rpc
.get_address_tx_ids(addresses, *range.start(), *range.end())
.get_address_tx_ids(AddressStrings::new(addresses), *range.start(), *range.end())
.await
.expect("arguments are valid so no error can happen here");
@ -482,3 +485,81 @@ async fn rpc_getaddresstxids_response_with(
.is_cancelled()
);
}
#[tokio::test]
async fn rpc_getaddressutxos_invalid_arguments() {
zebra_test::init();
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
let rpc = RpcImpl::new(
"RPC test",
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
Mainnet,
);
// call the method with an invalid address string
let address = "11111111".to_string();
let addresses = vec![address.clone()];
let error = rpc
.0
.get_address_utxos(AddressStrings::new(addresses))
.await
.unwrap_err();
assert_eq!(
error.message,
format!(
"invalid address \"{}\": parse error: t-addr decoding error",
address
)
);
mempool.expect_no_requests().await;
state.expect_no_requests().await;
}
#[tokio::test]
async fn rpc_getaddressutxos_response() {
zebra_test::init();
let blocks: Vec<Arc<Block>> = zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS
.iter()
.map(|(_height, block_bytes)| block_bytes.zcash_deserialize_into().unwrap())
.collect();
// get the first transaction of the first block
let first_block_first_transaction = &blocks[1].transactions[0];
// get the address, this is always `t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd`
let address = &first_block_first_transaction.outputs()[1]
.address(Mainnet)
.unwrap();
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
// Create a populated state service
let (_state, read_state, latest_chain_tip, _chain_tip_change) =
zebra_state::populated_state(blocks.clone(), Mainnet).await;
let rpc = RpcImpl::new(
"RPC test",
Buffer::new(mempool.clone(), 1),
Buffer::new(read_state.clone(), 1),
latest_chain_tip,
Mainnet,
);
// call the method with a valid address
let addresses = vec![address.to_string()];
let response = rpc
.0
.get_address_utxos(AddressStrings::new(addresses))
.await
.expect("address is valid so no error can happen here");
// there are 10 outputs for provided address
assert_eq!(response.len(), 10);
mempool.expect_no_requests().await;
}

View File

@ -462,4 +462,9 @@ pub enum ReadRequest {
/// The blocks to be queried for transactions.
height_range: RangeInclusive<block::Height>,
},
/// Looks up utxos for the provided addresses.
///
/// Returns a type with found utxos and transaction information.
UtxosByAddresses(HashSet<transparent::Address>),
}

View File

@ -13,7 +13,8 @@ use zebra_chain::{
// will work with inline links.
#[allow(unused_imports)]
use crate::Request;
use crate::TransactionLocation;
use crate::{service::read::AddressUtxos, TransactionLocation};
#[derive(Clone, Debug, PartialEq, Eq)]
/// A response to a [`StateService`] [`Request`].
@ -62,4 +63,7 @@ pub enum ReadResponse {
/// Response to [`ReadRequest::TransactionIdsByAddresses`] 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.
Utxos(AddressUtxos),
}

View File

@ -58,7 +58,7 @@ pub(crate) mod check;
mod finalized_state;
mod non_finalized_state;
mod pending_utxos;
mod read;
pub(crate) mod read;
#[cfg(any(test, feature = "proptest-impl"))]
pub mod arbitrary;
@ -1036,6 +1036,27 @@ impl Service<ReadRequest> for ReadStateService {
}
.boxed()
}
// For the get_address_utxos RPC.
ReadRequest::UtxosByAddresses(addresses) => {
metrics::counter!(
"state.requests",
1,
"service" => "read_state",
"type" => "utxos_by_addresses",
);
let state = self.clone();
async move {
let utxos = state.best_chain_receiver.with_watch_data(|best_chain| {
read::transparent_utxos(state.network, best_chain, &state.db, addresses)
});
utxos.map(ReadResponse::Utxos)
}
.boxed()
}
}
}
}