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 <teor@riseup.net>
This commit is contained in:
Alfredo Garcia 2022-04-13 05:48:13 -03:00 committed by GitHub
parent 43e80fd61c
commit 7b7d22aabc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 270 additions and 3 deletions

View File

@ -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<Result<GetRawTransaction>>;
/// 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<String>,
start: u32,
end: u32,
) -> BoxFuture<Result<Vec<String>>>;
}
/// RPC method implementations.
@ -555,6 +578,59 @@ where
}
.boxed()
}
fn get_address_tx_ids(
&self,
addresses: Vec<String>,
start: u32,
end: u32,
) -> BoxFuture<Result<Vec<String>>> {
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<Vec<Address>> = 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(())
}

View File

@ -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<Arc<Block>> = 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<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, 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));
}

View File

@ -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<Transaction>))`](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<transparent::Address>, block::Height, block::Height),
}

View File

@ -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<Transaction>, block::Height)>),
/// Response to [`ReadRequest::TransactionsByAddresses`] with the obtained transaction ids,
/// in the order they appear in blocks.
TransactionIds(Vec<Hash>),
}

View File

@ -970,7 +970,7 @@ impl Service<ReadRequest> 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<ReadRequest> 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()
}
}
}
}