feat(rpc): add getrawtransaction (#3908)
* feat(rpc): add getrawtransaction * Apply suggestions from code review Co-authored-by: teor <teor@riseup.net> Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com> * Apply suggestions from code review Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com> * address review comments * move SerializedTransaction to the right module * Use try_into() instead of 'as' * add proptests Co-authored-by: teor <teor@riseup.net> Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
parent
5d7f986183
commit
e7c0a78d4d
|
@ -5754,6 +5754,7 @@ dependencies = [
|
||||||
"jsonrpc-http-server",
|
"jsonrpc-http-server",
|
||||||
"proptest",
|
"proptest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
|
|
|
@ -25,6 +25,7 @@ pub use joinsplit::JoinSplitData;
|
||||||
pub use lock_time::LockTime;
|
pub use lock_time::LockTime;
|
||||||
pub use memo::Memo;
|
pub use memo::Memo;
|
||||||
pub use sapling::FieldNotPresent;
|
pub use sapling::FieldNotPresent;
|
||||||
|
pub use serialize::SerializedTransaction;
|
||||||
pub use sighash::{HashType, SigHash};
|
pub use sighash::{HashType, SigHash};
|
||||||
pub use unmined::{UnminedTx, UnminedTxId, VerifiedUnminedTx};
|
pub use unmined::{UnminedTx, UnminedTxId, VerifiedUnminedTx};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//! Contains impls of `ZcashSerialize`, `ZcashDeserialize` for all of the
|
//! Contains impls of `ZcashSerialize`, `ZcashDeserialize` for all of the
|
||||||
//! transaction types, so that all of the serialization logic is in one place.
|
//! 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 byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
use halo2::{arithmetic::FieldExt, pasta::pallas};
|
use halo2::{arithmetic::FieldExt, pasta::pallas};
|
||||||
|
@ -977,3 +977,36 @@ impl TrustedPreallocate for transparent::Output {
|
||||||
MAX_BLOCK_BYTES / MIN_TRANSPARENT_OUTPUT_SIZE
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ serde = { version = "1.0.136", features = ["serde_derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "0.10.1"
|
proptest = "0.10.1"
|
||||||
|
serde_json = "1.0.79"
|
||||||
thiserror = "1.0.30"
|
thiserror = "1.0.30"
|
||||||
tokio = { version = "1.16.1", features = ["full", "test-util"] }
|
tokio = { version = "1.16.1", features = ["full", "test-util"] }
|
||||||
|
|
||||||
|
|
|
@ -7,3 +7,5 @@
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod methods;
|
pub mod methods;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
//! Some parts of the `zcashd` RPC documentation are outdated.
|
//! Some parts of the `zcashd` RPC documentation are outdated.
|
||||||
//! So this implementation follows the `lightwalletd` client implementation.
|
//! So this implementation follows the `lightwalletd` client implementation.
|
||||||
|
|
||||||
|
use std::{collections::HashSet, io, sync::Arc};
|
||||||
|
|
||||||
use futures::{FutureExt, TryFutureExt};
|
use futures::{FutureExt, TryFutureExt};
|
||||||
use hex::{FromHex, ToHex};
|
use hex::{FromHex, ToHex};
|
||||||
use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
|
use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
|
||||||
|
@ -17,7 +19,7 @@ use zebra_chain::{
|
||||||
chain_tip::ChainTip,
|
chain_tip::ChainTip,
|
||||||
parameters::Network,
|
parameters::Network,
|
||||||
serialization::{SerializationError, ZcashDeserialize},
|
serialization::{SerializationError, ZcashDeserialize},
|
||||||
transaction::{self, Transaction},
|
transaction::{self, SerializedTransaction, Transaction},
|
||||||
};
|
};
|
||||||
use zebra_network::constants::USER_AGENT;
|
use zebra_network::constants::USER_AGENT;
|
||||||
use zebra_node_services::{mempool, BoxError};
|
use zebra_node_services::{mempool, BoxError};
|
||||||
|
@ -103,6 +105,30 @@ pub trait Rpc {
|
||||||
/// zcashd reference: [`getrawmempool`](https://zcash.github.io/rpc/getrawmempool.html)
|
/// zcashd reference: [`getrawmempool`](https://zcash.github.io/rpc/getrawmempool.html)
|
||||||
#[rpc(name = "getrawmempool")]
|
#[rpc(name = "getrawmempool")]
|
||||||
fn get_raw_mempool(&self) -> BoxFuture<Result<Vec<String>>>;
|
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.
|
/// RPC method implementations.
|
||||||
|
@ -321,6 +347,89 @@ where
|
||||||
}
|
}
|
||||||
.boxed()
|
.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)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
@ -362,3 +471,45 @@ pub struct GetBlock(#[serde(with = "hex")] SerializedBlock);
|
||||||
///
|
///
|
||||||
/// Also see the notes for the [`Rpc::get_best_block_hash` method].
|
/// Also see the notes for the [`Rpc::get_best_block_hash` method].
|
||||||
pub struct GetBestBlockHash(#[serde(with = "hex")] block::Hash);
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(GetRawTransaction::Raw(tx.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use zebra_chain::{
|
||||||
chain_tip::NoChainTip,
|
chain_tip::NoChainTip,
|
||||||
parameters::Network::*,
|
parameters::Network::*,
|
||||||
serialization::{ZcashDeserialize, ZcashSerialize},
|
serialization::{ZcashDeserialize, ZcashSerialize},
|
||||||
transaction::{Transaction, UnminedTx, UnminedTxId},
|
transaction::{self, Transaction, UnminedTx, UnminedTxId},
|
||||||
};
|
};
|
||||||
use zebra_node_services::mempool;
|
use zebra_node_services::mempool;
|
||||||
use zebra_state::BoxError;
|
use zebra_state::BoxError;
|
||||||
|
@ -322,6 +322,104 @@ proptest! {
|
||||||
Ok::<_, TestCaseError>(())
|
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)]
|
#[derive(Clone, Copy, Debug, Error)]
|
||||||
|
|
|
@ -5,8 +5,11 @@ use std::sync::Arc;
|
||||||
use tower::buffer::Buffer;
|
use tower::buffer::Buffer;
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::Block, chain_tip::NoChainTip, parameters::Network::*,
|
block::Block,
|
||||||
serialization::ZcashDeserializeInto,
|
chain_tip::NoChainTip,
|
||||||
|
parameters::Network::*,
|
||||||
|
serialization::{ZcashDeserializeInto, ZcashSerialize},
|
||||||
|
transaction::{UnminedTx, UnminedTxId},
|
||||||
};
|
};
|
||||||
use zebra_network::constants::USER_AGENT;
|
use zebra_network::constants::USER_AGENT;
|
||||||
use zebra_node_services::BoxError;
|
use zebra_node_services::BoxError;
|
||||||
|
@ -148,3 +151,84 @@ async fn rpc_getbestblockhash() {
|
||||||
|
|
||||||
mempool.expect_no_requests().await;
|
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.as_ref(), 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.as_ref(), tx.zcash_serialize_to_vec().unwrap());
|
||||||
|
} else {
|
||||||
|
unreachable!("Should return a Raw enum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
mod vectors;
|
|
@ -0,0 +1,19 @@
|
||||||
|
use crate::methods::GetRawTransaction;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_transaction_serialization() {
|
||||||
|
let expected_tx = GetRawTransaction::Raw(vec![0x42].into());
|
||||||
|
let expected_json = r#""42""#;
|
||||||
|
let j = serde_json::to_string(&expected_tx).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(j, expected_json);
|
||||||
|
|
||||||
|
let expected_tx = GetRawTransaction::Object {
|
||||||
|
hex: vec![0x42].into(),
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
let expected_json = r#"{"hex":"42","height":1}"#;
|
||||||
|
let j = serde_json::to_string(&expected_tx).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(j, expected_json);
|
||||||
|
}
|
|
@ -52,5 +52,5 @@ pub enum ReadResponse {
|
||||||
Block(Option<Arc<Block>>),
|
Block(Option<Arc<Block>>),
|
||||||
|
|
||||||
/// Response to [`ReadRequest::Transaction`] with the specified transaction.
|
/// Response to [`ReadRequest::Transaction`] with the specified transaction.
|
||||||
Transaction(Option<Arc<Transaction>>),
|
Transaction(Option<(Arc<Transaction>, block::Height)>),
|
||||||
}
|
}
|
||||||
|
|
|
@ -467,7 +467,7 @@ impl StateService {
|
||||||
/// Returns the [`Transaction`] with [`transaction::Hash`],
|
/// Returns the [`Transaction`] with [`transaction::Hash`],
|
||||||
/// if it exists in the current best chain.
|
/// if it exists in the current best chain.
|
||||||
pub fn best_transaction(&self, hash: transaction::Hash) -> Option<Arc<Transaction>> {
|
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.
|
/// Return the hash for the block at `height` in the current best chain.
|
||||||
|
@ -957,11 +957,12 @@ impl Service<ReadRequest> for ReadStateService {
|
||||||
let state = self.clone();
|
let state = self.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let transaction = state.best_chain_receiver.with_watch_data(|best_chain| {
|
let transaction_and_height =
|
||||||
|
state.best_chain_receiver.with_watch_data(|best_chain| {
|
||||||
read::transaction(best_chain, &state.db, hash)
|
read::transaction(best_chain, &state.db, hash)
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(ReadResponse::Transaction(transaction))
|
Ok(ReadResponse::Transaction(transaction_and_height))
|
||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,10 @@ impl ZebraDb {
|
||||||
|
|
||||||
/// Returns the [`Transaction`] with [`transaction::Hash`],
|
/// Returns the [`Transaction`] with [`transaction::Hash`],
|
||||||
/// if it exists in the finalized chain.
|
/// 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)> {
|
||||||
self.transaction_location(hash)
|
self.transaction_location(hash)
|
||||||
.map(|TransactionLocation { index, height }| {
|
.map(|TransactionLocation { index, height }| {
|
||||||
let block = self
|
let block = self
|
||||||
|
@ -119,7 +122,7 @@ impl ZebraDb {
|
||||||
.expect("block will exist if TransactionLocation does");
|
.expect("block will exist if TransactionLocation does");
|
||||||
|
|
||||||
// TODO: store transactions in a separate database index (#3151)
|
// TODO: store transactions in a separate database index (#3151)
|
||||||
block.transactions[index.as_usize()].clone()
|
(block.transactions[index.as_usize()].clone(), height)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -330,10 +330,13 @@ impl Chain {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the [`Transaction`] with [`transaction::Hash`], if it exists in this 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
|
self.tx_by_hash
|
||||||
.get(&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.
|
/// Returns the block hash of the tip block.
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::Block,
|
block::{self, Block},
|
||||||
transaction::{self, Transaction},
|
transaction::{self, Transaction},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ pub(crate) fn transaction<C>(
|
||||||
chain: Option<C>,
|
chain: Option<C>,
|
||||||
db: &ZebraDb,
|
db: &ZebraDb,
|
||||||
hash: transaction::Hash,
|
hash: transaction::Hash,
|
||||||
) -> Option<Arc<Transaction>>
|
) -> Option<(Arc<Transaction>, block::Height)>
|
||||||
where
|
where
|
||||||
C: AsRef<Chain>,
|
C: AsRef<Chain>,
|
||||||
{
|
{
|
||||||
|
@ -65,6 +65,11 @@ where
|
||||||
// (`chain` is always in memory, but `db` stores transactions on disk, with a memory cache.)
|
// (`chain` is always in memory, but `db` stores transactions on disk, with a memory cache.)
|
||||||
chain
|
chain
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|chain| chain.as_ref().transaction(hash).cloned())
|
.and_then(|chain| {
|
||||||
|
chain
|
||||||
|
.as_ref()
|
||||||
|
.transaction(hash)
|
||||||
|
.map(|(tx, height)| (tx.clone(), height))
|
||||||
|
})
|
||||||
.or_else(|| db.transaction(hash))
|
.or_else(|| db.transaction(hash))
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,10 @@ async fn populated_read_state_responds_correctly() -> Result<()> {
|
||||||
for transaction in &block.transactions {
|
for transaction in &block.transactions {
|
||||||
let transaction_cases = vec![(
|
let transaction_cases = vec![(
|
||||||
ReadRequest::Transaction(transaction.hash()),
|
ReadRequest::Transaction(transaction.hash()),
|
||||||
Ok(ReadResponse::Transaction(Some(transaction.clone()))),
|
Ok(ReadResponse::Transaction(Some((
|
||||||
|
transaction.clone(),
|
||||||
|
block.coinbase_height().unwrap(),
|
||||||
|
)))),
|
||||||
)];
|
)];
|
||||||
|
|
||||||
let transaction_cases = Transcript::from(transaction_cases);
|
let transaction_cases = Transcript::from(transaction_cases);
|
||||||
|
|
Loading…
Reference in New Issue