Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rpc): Implement what we can of getaddresstxids RPC method. #4062

Merged
merged 13 commits into from
Apr 13, 2022
95 changes: 95 additions & 0 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(())
}
134 changes: 134 additions & 0 deletions zebra-rpc/src/methods/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
.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));
}
13 changes: 12 additions & 1 deletion zebra-state/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
}
6 changes: 5 additions & 1 deletion zebra-state/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::Arc;

use zebra_chain::{
block::{self, Block},
transaction::Transaction,
transaction::{Hash, Transaction},
transparent,
};

Expand Down Expand Up @@ -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>),
}
25 changes: 24 additions & 1 deletion zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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()
}
}
}
}
Expand Down