From 7b7d22aabc3148d6d8857e615a4ab1a04924ba1f Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Wed, 13 Apr 2022 05:48:13 -0300 Subject: [PATCH] feat(rpc): Implement what we can of `getaddresstxids` RPC method. (#4062) * implement `getaddresstxids` rpc method with dummy empty response * use already public function * fix some docs * pass a list of addresses to the state request * sync range errors with zcashd * refactor a loop * fix grammar * fix tests Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: teor --- zebra-rpc/src/methods.rs | 95 ++++++++++++++++++ zebra-rpc/src/methods/tests/vectors.rs | 134 +++++++++++++++++++++++++ zebra-state/src/request.rs | 13 ++- zebra-state/src/response.rs | 6 +- zebra-state/src/service.rs | 25 ++++- 5 files changed, 270 insertions(+), 3 deletions(-) diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 80f36d8449c..59c80865b76 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -24,6 +24,7 @@ use zebra_chain::{ parameters::{ConsensusBranchId, Network, NetworkUpgrade}, serialization::{SerializationError, ZcashDeserialize}, transaction::{self, SerializedTransaction, Transaction, UnminedTx}, + transparent::Address, }; use zebra_network::constants::USER_AGENT; use zebra_node_services::{mempool, BoxError}; @@ -144,6 +145,28 @@ pub trait Rpc { txid_hex: String, verbose: u8, ) -> BoxFuture>; + + /// Returns the transaction ids made by the provided transparent addresses. + /// + /// zcashd reference: [`getaddresstxids`](https://zcash.github.io/rpc/getaddresstxids.html) + /// + /// # Parameters + /// + /// - `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, + addresses: Vec, + start: u32, + end: u32, + ) -> BoxFuture>>; } /// RPC method implementations. @@ -555,6 +578,59 @@ where } .boxed() } + + fn get_address_tx_ids( + &self, + addresses: Vec, + start: u32, + end: u32, + ) -> BoxFuture>> { + let mut state = self.state.clone(); + let mut response_transactions = vec![]; + let start = Height(start); + let end = Height(end); + + let chain_height = self.latest_chain_tip.best_tip_height().ok_or(Error { + code: ErrorCode::ServerError(0), + message: "No blocks in state".to_string(), + data: None, + }); + + async move { + // 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 request = + zebra_state::ReadRequest::TransactionsByAddresses(valid_addresses?, 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, + })?; + + match response { + zebra_state::ReadResponse::TransactionIds(hashes) => response_transactions + .append(&mut hashes.iter().map(|h| h.to_string()).collect()), + _ => unreachable!("unmatched response to a TransactionsByAddresses request"), + } + + Ok(response_transactions) + } + .boxed() + } } /// Response to a `getinfo` RPC request. @@ -679,3 +755,22 @@ impl GetRawTransaction { } } } + +/// Check if provided height range is valid +fn check_height_range(start: Height, end: Height, chain_height: Height) -> Result<()> { + if start == Height(0) || end == Height(0) { + return Err(Error::invalid_params( + "Start and end are expected to be greater than zero", + )); + } + if end < start { + return Err(Error::invalid_params( + "End value is expected to be greater than or equal to start", + )); + } + if start > chain_height || end > chain_height { + return Err(Error::invalid_params("Start or end is outside chain range")); + } + + Ok(()) +} diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 99143f371c1..f2799a6cd14 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -305,3 +305,137 @@ async fn rpc_getrawtransaction() { let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never(); assert!(matches!(rpc_tx_queue_task_result, None)); } + +#[tokio::test] +async fn rpc_getaddresstxids_invalid_arguments() { + zebra_test::init(); + + let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests(); + + // Create a continuous chain of mainnet blocks from genesis + let blocks: Vec> = zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS + .iter() + .map(|(_height, block_bytes)| block_bytes.zcash_deserialize_into().unwrap()) + .collect(); + + // 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, rpc_tx_queue_task_handle) = RpcImpl::new( + "RPC test", + Buffer::new(mempool.clone(), 1), + Buffer::new(read_state.clone(), 1), + latest_chain_tip, + Mainnet, + ); + + // call the method with an invalid address string + let address = "11111111".to_string(); + let addresses = vec![address.clone()]; + let start: u32 = 1; + let end: u32 = 2; + let error = rpc + .get_address_tx_ids(addresses, start, end) + .await + .unwrap_err(); + assert_eq!( + error.message, + format!("Provided address is not valid: {}", address) + ); + + // create a valid address + let address = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string(); + let addresses = vec![address.clone()]; + + // call the method with start greater than end + let start: u32 = 2; + let end: u32 = 1; + let error = rpc + .get_address_tx_ids(addresses.clone(), start, end) + .await + .unwrap_err(); + assert_eq!( + error.message, + "End value is expected to be greater than or equal to start".to_string() + ); + + // call the method with start equal zero + let start: u32 = 0; + let end: u32 = 1; + let error = rpc + .get_address_tx_ids(addresses.clone(), start, end) + .await + .unwrap_err(); + assert_eq!( + error.message, + "Start and end are expected to be greater than zero".to_string() + ); + + // call the method outside the chain tip height + let start: u32 = 1; + let end: u32 = 11; + let error = rpc + .get_address_tx_ids(addresses, start, end) + .await + .unwrap_err(); + assert_eq!( + error.message, + "Start or end is outside chain range".to_string() + ); + + mempool.expect_no_requests().await; + + // The queue task should continue without errors or panics + let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never(); + assert!(matches!(rpc_tx_queue_task_result, None)); +} + +#[tokio::test] +async fn rpc_getaddresstxids_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, rpc_tx_queue_task_handle) = RpcImpl::new( + "RPC test", + Buffer::new(mempool.clone(), 1), + Buffer::new(read_state.clone(), 1), + latest_chain_tip, + Mainnet, + ); + + // call the method with valid arguments + let addresses = vec![address.to_string()]; + let start: u32 = 1; + let end: u32 = 1; + let response = rpc + .get_address_tx_ids(addresses, start, end) + .await + .expect("arguments are valid so no error can happen here"); + + // TODO: The lenght of the response should be 1 + // Fix in the context of #3147 + assert_eq!(response.len(), 0); + + mempool.expect_no_requests().await; + + // The queue task should continue without errors or panics + let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never(); + assert!(matches!(rpc_tx_queue_task_result, None)); +} diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index f56e8de3def..f8bc64b91ee 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -413,7 +413,7 @@ pub enum Request { }, } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] /// A read-only query about the chain state, via the [`ReadStateService`]. pub enum ReadRequest { /// Looks up a block by hash or height in the current best chain. @@ -434,4 +434,15 @@ pub enum ReadRequest { /// * [`Response::Transaction(Some(Arc))`](Response::Transaction) if the transaction is in the best chain; /// * [`Response::Transaction(None)`](Response::Transaction) otherwise. Transaction(transaction::Hash), + + /// Looks up transactions hashes that were made by provided addresses in a blockchain height range. + /// + /// Returns + /// + /// * A vector of transaction hashes. + /// * An empty vector if no transactions were found for the given arguments. + /// + /// Returned txids are in the order they appear in blocks, which ensures that they are topologically sorted + /// (i.e. parent txids will appear before child txids). + TransactionsByAddresses(Vec, block::Height, block::Height), } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 6377142550c..6a0af14a018 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use zebra_chain::{ block::{self, Block}, - transaction::Transaction, + transaction::{Hash, Transaction}, transparent, }; @@ -53,4 +53,8 @@ pub enum ReadResponse { /// Response to [`ReadRequest::Transaction`] with the specified transaction. Transaction(Option<(Arc, block::Height)>), + + /// Response to [`ReadRequest::TransactionsByAddresses`] with the obtained transaction ids, + /// in the order they appear in blocks. + TransactionIds(Vec), } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 47567b1d552..0b790cee539 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -970,7 +970,7 @@ impl Service for ReadStateService { .boxed() } - // For the get_raw_transaction RPC, to be implemented in #3145. + // For the get_raw_transaction RPC. ReadRequest::Transaction(hash) => { metrics::counter!( "state.requests", @@ -991,6 +991,29 @@ impl Service for ReadStateService { } .boxed() } + + // For the get_address_tx_ids RPC. + ReadRequest::TransactionsByAddresses(_addresses, _start, _end) => { + metrics::counter!( + "state.requests", + 1, + "service" => "read_state", + "type" => "transactions_by_addresses", + ); + + let _state = self.clone(); + + async move { + // TODO: Respond with found transactions + // At least the following pull requests should be merged: + // - #4022 + // - #4038 + // Do the corresponding update in the context of #3147 + let transaction_ids = vec![]; + Ok(ReadResponse::TransactionIds(transaction_ids)) + } + .boxed() + } } } }