From 56aabb1db1e6f5df179644356f6bdaad58cbb9d9 Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Mon, 25 Apr 2022 00:00:52 -0300 Subject: [PATCH] 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> --- zebra-chain/src/transparent/script.rs | 28 +++++ zebra-rpc/src/methods.rs | 136 +++++++++++++++++++++---- zebra-rpc/src/methods/tests/vectors.rs | 93 +++++++++++++++-- zebra-state/src/request.rs | 5 + zebra-state/src/response.rs | 6 +- zebra-state/src/service.rs | 23 ++++- 6 files changed, 261 insertions(+), 30 deletions(-) diff --git a/zebra-chain/src/transparent/script.rs b/zebra-chain/src/transparent/script.rs index c2517285..b6623496 100644 --- a/zebra-chain/src/transparent/script.rs +++ b/zebra-chain/src/transparent/script.rs @@ -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::()) + } +} + 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>(&self) -> T { + self.as_raw_bytes().encode_hex() + } + + fn encode_hex_upper>(&self) -> T { + self.as_raw_bytes().encode_hex_upper() + } +} + +impl ToHex for Script { + fn encode_hex>(&self) -> T { + (&self).encode_hex() + } + + fn encode_hex_upper>(&self) -> T { + (&self).encode_hex_upper() + } +} + impl ZcashSerialize for Script { fn zcash_serialize(&self, writer: W) -> Result<(), io::Error> { zcash_serialize_bytes(&self.0, writer) diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 1c1076b1..83387ddf 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -190,10 +190,28 @@ pub trait Rpc { #[rpc(name = "getaddresstxids")] fn get_address_tx_ids( &self, - addresses: Vec, + address_strings: AddressStrings, start: u32, end: u32, ) -> BoxFuture>>; + + /// 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>>; } /// RPC method implementations. @@ -403,17 +421,9 @@ where let state = self.state.clone(); async move { - let addresses: HashSet
= address_strings - .addresses - .into_iter() - .map(|address| { - address.parse().map_err(|error| { - Error::invalid_params(&format!("invalid address {address:?}: {error}")) - }) - }) - .collect::>()?; + 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, + address_strings: AddressStrings, start: u32, end: u32, ) -> BoxFuture>> { @@ -660,17 +670,10 @@ where // height range checks check_height_range(start, end, chain_height?)?; - let valid_addresses: Result> = 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>> { + 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, } +impl AddressStrings { + // Creates a new `AddressStrings` given a vector. + #[cfg(test)] + pub fn new(addresses: Vec) -> 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> { + let valid_addresses: HashSet
= self + .addresses + .into_iter() + .map(|address| { + address.parse().map_err(|error| { + Error::invalid_params(&format!("invalid address {address:?}: {error}")) + }) + }) + .collect::>()?; + + 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, diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 16c40aac..65f363b1 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -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> = 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; +} diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 7630e92e..33563fde 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -462,4 +462,9 @@ pub enum ReadRequest { /// The blocks to be queried for transactions. height_range: RangeInclusive, }, + + /// Looks up utxos for the provided addresses. + /// + /// Returns a type with found utxos and transaction information. + UtxosByAddresses(HashSet), } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 4e9797e5..dc2eee5e 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -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), + + /// Response to [`ReadRequest::UtxosByAddresses`] with found utxos and transaction data. + Utxos(AddressUtxos), } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 3cd3fbff..bd3b045c 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -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 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() + } } } }