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): add getrawtransaction #3908

Merged
merged 8 commits into from
Mar 24, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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-chain/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub use joinsplit::JoinSplitData;
pub use lock_time::LockTime;
pub use memo::Memo;
pub use sapling::FieldNotPresent;
pub use serialize::SerializedTransaction;
pub use sighash::{HashType, SigHash};
pub use unmined::{UnminedTx, UnminedTxId, VerifiedUnminedTx};

Expand Down
35 changes: 34 additions & 1 deletion zebra-chain/src/transaction/serialize.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Contains impls of `ZcashSerialize`, `ZcashDeserialize` for all of the
//! transaction types, so that all of the serialization logic is in one place.

use std::{convert::TryInto, io, sync::Arc};
use std::{borrow::Borrow, convert::TryInto, io, sync::Arc};

use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use halo2::{arithmetic::FieldExt, pasta::pallas};
Expand Down Expand Up @@ -977,3 +977,36 @@ impl TrustedPreallocate for transparent::Output {
MAX_BLOCK_BYTES / MIN_TRANSPARENT_OUTPUT_SIZE
}
}

/// A serialized transaction.
///
/// Stores bytes that are guaranteed to be deserializable into a [`Transaction`].
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct SerializedTransaction {
bytes: Vec<u8>,
}

/// Build a [`SerializedTransaction`] by serializing a block.
impl<B: Borrow<Transaction>> From<B> for SerializedTransaction {
fn from(tx: B) -> Self {
SerializedTransaction {
bytes: tx
.borrow()
.zcash_serialize_to_vec()
.expect("Writing to a `Vec` should never fail"),
}
}
}

/// Access the serialized bytes of a [`SerializedTransaction`].
impl AsRef<[u8]> for SerializedTransaction {
fn as_ref(&self) -> &[u8] {
self.bytes.as_ref()
}
}

impl From<Vec<u8>> for SerializedTransaction {
fn from(bytes: Vec<u8>) -> Self {
Self { bytes }
}
}
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;
153 changes: 152 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 @@ -17,7 +19,7 @@ use zebra_chain::{
chain_tip::ChainTip,
parameters::Network,
serialization::{SerializationError, ZcashDeserialize},
transaction::{self, Transaction},
transaction::{self, SerializedTransaction, Transaction},
};
use zebra_network::constants::USER_AGENT;
use zebra_node_services::{mempool, BoxError};
Expand Down Expand Up @@ -103,6 +105,30 @@ 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.
///
/// # Notes
///
/// We don't currently support the `blockhash` parameter since lightwalletd does not
/// use it.
///
/// In verbose mode, we only expose the `hex` and `height` fields since
/// lightwalletd uses only those:
/// <https://github.com/zcash/lightwalletd/blob/631bb16404e3d8b045e74a7c5489db626790b2f6/common/common.go#L119>
#[rpc(name = "getrawtransaction")]
fn get_raw_transaction(
&self,
txid_hex: String,
verbose: u8,
) -> BoxFuture<Result<GetRawTransaction>>;
}

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

fn get_raw_transaction(
&self,
txid_hex: 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(txid_hex).map_err(|_| {
Error::invalid_params("transaction ID is not specified as a hex string")
})?;

// Check the mempool first.
//
// # Correctness
//
// Transactions are removed from the mempool after they are mined into blocks,
// so the transaction could be just in the mempool, just in the state, or in both.
// (And the mempool and state transactions could have different authorising data.)
// But it doesn't matter which transaction we choose, because the effects are the same.
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 +471,45 @@ 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].
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(untagged)]
pub enum GetRawTransaction {
/// The raw transaction, encoded as hex bytes.
Raw(#[serde(with = "hex")] SerializedTransaction),
/// The transaction object.
Object {
/// The raw transaction, encoded as hex bytes.
#[serde(with = "hex")]
hex: SerializedTransaction,
/// The height of the block that contains the transaction, or -1 if
/// not applicable.
height: i32,
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
},
}

impl GetRawTransaction {
fn from_transaction(
tx: Arc<Transaction>,
height: Option<block::Height>,
verbose: bool,
) -> std::result::Result<Self, io::Error> {
if verbose {
Ok(GetRawTransaction::Object {
hex: tx.into(),
height: match height {
Some(height) => height
.0
.try_into()
.expect("valid block heights are limited to i32::MAX"),
None => -1,
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
},
})
} else {
Ok(GetRawTransaction::Raw(tx.into()))
}
}
}
100 changes: 99 additions & 1 deletion zebra-rpc/src/methods/tests/prop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use zebra_chain::{
chain_tip::NoChainTip,
parameters::Network::*,
serialization::{ZcashDeserialize, ZcashSerialize},
transaction::{Transaction, UnminedTx, UnminedTxId},
transaction::{self, Transaction, UnminedTx, UnminedTxId},
};
use zebra_node_services::mempool;
use zebra_state::BoxError;
Expand Down Expand Up @@ -322,6 +322,104 @@ proptest! {
Ok::<_, TestCaseError>(())
})?;
}

/// Test that the method rejects non-hexadecimal characters.
///
/// Try to call `get_raw_transaction` using a string parameter that has at least one
/// non-hexadecimal character, and check that it fails with an expected error.
#[test]
fn get_raw_transaction_non_hexadecimal_string_results_in_an_error(non_hex_string in ".*[^0-9A-Fa-f].*") {
let runtime = zebra_test::init_async();
let _guard = runtime.enter();

// CORRECTNESS: Nothing in this test depends on real time, so we can speed it up.
tokio::time::pause();

runtime.block_on(async move {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();

let rpc = RpcImpl::new(
"RPC test",
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
Mainnet,
);

let send_task = tokio::spawn(rpc.get_raw_transaction(non_hex_string, 0));

mempool.expect_no_requests().await?;
state.expect_no_requests().await?;

let result = send_task
.await
.expect("Sending raw transactions should not panic");

prop_assert!(
matches!(
result,
Err(Error {
code: ErrorCode::InvalidParams,
..
})
),
"Result is not an invalid parameters error: {result:?}"
);

Ok::<_, TestCaseError>(())
})?;
}

/// Test that the method rejects an input that's not a transaction.
///
/// Try to call `get_raw_transaction` using random bytes that fail to deserialize as a
/// transaction, and check that it fails with an expected error.
#[test]
fn get_raw_transaction_invalid_transaction_results_in_an_error(random_bytes in any::<Vec<u8>>()) {
let runtime = zebra_test::init_async();
let _guard = runtime.enter();

// CORRECTNESS: Nothing in this test depends on real time, so we can speed it up.
tokio::time::pause();

prop_assume!(transaction::Hash::zcash_deserialize(&*random_bytes).is_err());

runtime.block_on(async move {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();

let rpc = RpcImpl::new(
"RPC test",
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
Mainnet,
);

let send_task = tokio::spawn(rpc.get_raw_transaction(hex::encode(random_bytes), 0));

mempool.expect_no_requests().await?;
state.expect_no_requests().await?;

let result = send_task
.await
.expect("Sending raw transactions should not panic");

prop_assert!(
matches!(
result,
Err(Error {
code: ErrorCode::InvalidParams,
..
})
),
"Result is not an invalid parameters error: {result:?}"
);

Ok::<_, TestCaseError>(())
})?;
}
}

#[derive(Clone, Copy, Debug, Error)]
Expand Down
Loading