Skip to content

Commit

Permalink
feat(rpc): add getrawtransaction
Browse files Browse the repository at this point in the history
  • Loading branch information
conradoplg committed Mar 18, 2022
1 parent 9bf5ca0 commit 41c56d2
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 16 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions zebra-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ serde = { version = "1.0.136", features = ["serde_derive"] }

[dev-dependencies]
proptest = "0.10.1"
serde_json = "1.0.79"
thiserror = "1.0.30"
tokio = { version = "1.16.1", features = ["full", "test-util"] }

Expand Down
2 changes: 2 additions & 0 deletions zebra-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
pub mod config;
pub mod methods;
pub mod server;
#[cfg(test)]
mod tests;
140 changes: 139 additions & 1 deletion zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
//! Some parts of the `zcashd` RPC documentation are outdated.
//! So this implementation follows the `lightwalletd` client implementation.
use std::{collections::HashSet, io, sync::Arc};

use futures::{FutureExt, TryFutureExt};
use hex::{FromHex, ToHex};
use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
Expand All @@ -16,7 +18,7 @@ use zebra_chain::{
block::{self, SerializedBlock},
chain_tip::ChainTip,
parameters::Network,
serialization::{SerializationError, ZcashDeserialize},
serialization::{SerializationError, ZcashDeserialize, ZcashSerialize},
transaction::{self, Transaction},
};
use zebra_network::constants::USER_AGENT;
Expand Down Expand Up @@ -103,6 +105,22 @@ pub trait Rpc {
/// zcashd reference: [`getrawmempool`](https://zcash.github.io/rpc/getrawmempool.html)
#[rpc(name = "getrawmempool")]
fn get_raw_mempool(&self) -> BoxFuture<Result<Vec<String>>>;

/// Returns the raw transaction data, as a [`GetRawTransaction`] JSON string or structure.
///
/// zcashd reference: [`getrawtransaction`](https://zcash.github.io/rpc/getrawtransaction.html)
///
/// # Parameters
///
/// - `txid`: (string, required) The transaction ID of the transaction to be returned.
/// - `verbose`: (numeric, optional, default=0) If 0, return a string of hex-encoded data, otherwise return a JSON object.
/// - `blockhash`: (string, optional) The block in which to look for the transaction.
#[rpc(name = "getrawtransaction")]
fn get_raw_transaction(
&self,
txid: String,
verbose: u8,
) -> BoxFuture<Result<GetRawTransaction>>;
}

/// RPC method implementations.
Expand Down Expand Up @@ -321,6 +339,85 @@ where
}
.boxed()
}

fn get_raw_transaction(
&self,
hex_txid: String,
verbose: u8,
) -> BoxFuture<Result<GetRawTransaction>> {
let mut state = self.state.clone();
let mut mempool = self.mempool.clone();

async move {
let txid = transaction::Hash::from_hex(hex_txid).map_err(|error| Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
})?;

// Check the mempool first
let mut txid_set = HashSet::new();
txid_set.insert(txid);
let request = mempool::Request::TransactionsByMinedId(txid_set);

let response = mempool
.ready()
.and_then(|service| service.call(request))
.await
.map_err(|error| Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
})?;

match response {
mempool::Response::Transactions(unmined_transactions) => {
if !unmined_transactions.is_empty() {
let tx = unmined_transactions[0].transaction.clone();
return GetRawTransaction::from_transaction(tx, None, verbose != 0)
.map_err(|error| Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
});
}
}
_ => unreachable!("unmatched response to a transactionids request"),
};

// Now check the state

let request = zebra_state::ReadRequest::Transaction(txid);
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::Transaction(Some((tx, height))) => Ok(
GetRawTransaction::from_transaction(tx, Some(height), verbose != 0).map_err(
|error| Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
},
)?,
),
zebra_state::ReadResponse::Transaction(None) => Err(Error {
code: ErrorCode::ServerError(0),
message: "Transaction not found".to_string(),
data: None,
}),
_ => unreachable!("unmatched response to a transaction request"),
}
}
.boxed()
}
}

#[derive(serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -362,3 +459,44 @@ pub struct GetBlock(#[serde(with = "hex")] SerializedBlock);
///
/// Also see the notes for the [`Rpc::get_best_block_hash` method].
pub struct GetBestBlockHash(#[serde(with = "hex")] block::Hash);

/// Response to a `getrawtransaction` RPC request.
///
/// See the notes for the [`Rpc::get_raw_transaction` method].
// pub struct GetRawTransaction(pub(super) SerializedTransaction);
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum GetRawTransaction {
/// The raw transaction, encoded as hex bytes.
Raw(#[serde(with = "hex")] Vec<u8>),
/// The transaction object.
Object {
/// The raw transaction, encoded as hex bytes.
#[serde(with = "hex")]
hex: Vec<u8>,
/// The height of the block that contains the transaction, or -1 if
/// not applicable.
height: i32,
},
}

impl GetRawTransaction {
fn from_transaction(
tx: Arc<Transaction>,
height: Option<block::Height>,
verbose: bool,
) -> std::result::Result<Self, io::Error> {
let serialized_tx = tx.zcash_serialize_to_vec()?;
if verbose {
Ok(GetRawTransaction::Object {
hex: serialized_tx,
height: match height {
Some(height) => height.0 as i32,
None => -1,
},
})
} else {
Ok(GetRawTransaction::Raw(serialized_tx))
}
}
}
86 changes: 85 additions & 1 deletion zebra-rpc/src/methods/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ use std::sync::Arc;
use tower::buffer::Buffer;

use zebra_chain::{
block::Block, chain_tip::NoChainTip, parameters::Network::*,
block::Block,
chain_tip::NoChainTip,
parameters::Network::*,
serialization::ZcashDeserializeInto,
transaction::{UnminedTx, UnminedTxId},
};
use zebra_network::constants::USER_AGENT;
use zebra_node_services::BoxError;
Expand Down Expand Up @@ -148,3 +151,84 @@ async fn rpc_getbestblockhash() {

mempool.expect_no_requests().await;
}

#[tokio::test]
async fn rpc_getrawtransaction() {
zebra_test::init();

// 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();

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;

// Init RPC
let rpc = RpcImpl::new(
"RPC test",
Buffer::new(mempool.clone(), 1),
read_state,
latest_chain_tip,
Mainnet,
);

// Test case where transaction is in mempool.
// Skip genesis because its tx is not indexed.
for block in blocks.iter().skip(1) {
for tx in block.transactions.iter() {
let mempool_req = mempool
.expect_request_that(|request| {
if let mempool::Request::TransactionsByMinedId(ids) = request {
ids.len() == 1 && ids.contains(&tx.hash())
} else {
false
}
})
.map(|responder| {
responder.respond(mempool::Response::Transactions(vec![UnminedTx {
id: UnminedTxId::Legacy(tx.hash()),
transaction: tx.clone(),
size: 0,
}]));
});
let get_tx_req = rpc.get_raw_transaction(tx.hash().encode_hex(), 0u8);
let (response, _) = futures::join!(get_tx_req, mempool_req);
let get_tx = response.expect("We should have a GetRawTransaction struct");
if let GetRawTransaction::Raw(raw_tx) = get_tx {
assert_eq!(raw_tx, tx.zcash_serialize_to_vec().unwrap());
} else {
unreachable!("Should return a Raw enum")
}
}
}

// Test case where transaction is _not_ in mempool.
// Skip genesis because its tx is not indexed.
for block in blocks.iter().skip(1) {
for tx in block.transactions.iter() {
let mempool_req = mempool
.expect_request_that(|request| {
if let mempool::Request::TransactionsByMinedId(ids) = request {
ids.len() == 1 && ids.contains(&tx.hash())
} else {
false
}
})
.map(|responder| {
responder.respond(mempool::Response::Transactions(vec![]));
});
let get_tx_req = rpc.get_raw_transaction(tx.hash().encode_hex(), 0u8);
let (response, _) = futures::join!(get_tx_req, mempool_req);
let get_tx = response.expect("We should have a GetRawTransaction struct");
if let GetRawTransaction::Raw(raw_tx) = get_tx {
assert_eq!(raw_tx, tx.zcash_serialize_to_vec().unwrap());
} else {
unreachable!("Should return a Raw enum")
}
}
}
}
1 change: 1 addition & 0 deletions zebra-rpc/src/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod vectors;
27 changes: 27 additions & 0 deletions zebra-rpc/src/tests/vectors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use crate::methods::GetRawTransaction;

#[test]
pub fn test_transaction_serialization() {
let expected_tx = GetRawTransaction::Raw(vec![0x42]);
let expected_json = r#""42""#;
let j = serde_json::to_string(&expected_tx).unwrap();

assert_eq!(j, expected_json);

let tx: GetRawTransaction = serde_json::from_str(&j).unwrap();

assert_eq!(tx, expected_tx);

let expected_tx = GetRawTransaction::Object {
hex: vec![0x42],
height: 1,
};
let expected_json = r#"{"hex":"42","height":1}"#;
let j = serde_json::to_string(&expected_tx).unwrap();

assert_eq!(j, expected_json);

let tx: GetRawTransaction = serde_json::from_str(&j).unwrap();

assert_eq!(tx, expected_tx);
}
2 changes: 1 addition & 1 deletion zebra-state/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@ pub enum ReadResponse {
Block(Option<Arc<Block>>),

/// Response to [`ReadRequest::Transaction`] with the specified transaction.
Transaction(Option<Arc<Transaction>>),
Transaction(Option<(Arc<Transaction>, block::Height)>),
}
11 changes: 6 additions & 5 deletions zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ impl StateService {
/// Returns the [`Transaction`] with [`transaction::Hash`],
/// if it exists in the current best chain.
pub fn best_transaction(&self, hash: transaction::Hash) -> Option<Arc<Transaction>> {
read::transaction(self.mem.best_chain(), self.disk.db(), hash)
read::transaction(self.mem.best_chain(), self.disk.db(), hash).map(|(tx, _height)| tx)
}

/// Return the hash for the block at `height` in the current best chain.
Expand Down Expand Up @@ -957,11 +957,12 @@ impl Service<ReadRequest> for ReadStateService {
let state = self.clone();

async move {
let transaction = state.best_chain_receiver.with_watch_data(|best_chain| {
read::transaction(best_chain, &state.db, hash)
});
let transaction_and_height =
state.best_chain_receiver.with_watch_data(|best_chain| {
read::transaction(best_chain, &state.db, hash)
});

Ok(ReadResponse::Transaction(transaction))
Ok(ReadResponse::Transaction(transaction_and_height))
}
.boxed()
}
Expand Down
7 changes: 5 additions & 2 deletions zebra-state/src/service/finalized_state/zebra_db/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ impl ZebraDb {

/// Returns the [`Transaction`] with [`transaction::Hash`],
/// if it exists in the finalized chain.
pub fn transaction(&self, hash: transaction::Hash) -> Option<Arc<Transaction>> {
pub fn transaction(
&self,
hash: transaction::Hash,
) -> Option<(Arc<Transaction>, block::Height)> {
let tx_by_hash = self.db.cf_handle("tx_by_hash").unwrap();
self.db
.zs_get(tx_by_hash, &hash)
Expand All @@ -114,7 +117,7 @@ impl ZebraDb {
.expect("block will exist if TransactionLocation does");

// TODO: store transactions in a separate database index (#3151)
block.transactions[index.as_usize()].clone()
(block.transactions[index.as_usize()].clone(), height)
})
}

Expand Down
7 changes: 5 additions & 2 deletions zebra-state/src/service/non_finalized_state/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,13 @@ impl Chain {
}

/// Returns the [`Transaction`] with [`transaction::Hash`], if it exists in this chain.
pub fn transaction(&self, hash: transaction::Hash) -> Option<&Arc<Transaction>> {
pub fn transaction(
&self,
hash: transaction::Hash,
) -> Option<(&Arc<Transaction>, block::Height)> {
self.tx_by_hash
.get(&hash)
.map(|(height, index)| &self.blocks[height].block.transactions[*index])
.map(|(height, index)| (&self.blocks[height].block.transactions[*index], *height))
}

/// Returns the block hash of the tip block.
Expand Down
Loading

0 comments on commit 41c56d2

Please sign in to comment.