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",
|
||||
"proptest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tower",
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
||||
|
|
|
@ -7,3 +7,5 @@
|
|||
pub mod config;
|
||||
pub mod methods;
|
||||
pub mod server;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
|
@ -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};
|
||||
|
@ -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};
|
||||
|
@ -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.
|
||||
|
@ -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)]
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
parameters::Network::*,
|
||||
serialization::{ZcashDeserialize, ZcashSerialize},
|
||||
transaction::{Transaction, UnminedTx, UnminedTxId},
|
||||
transaction::{self, Transaction, UnminedTx, UnminedTxId},
|
||||
};
|
||||
use zebra_node_services::mempool;
|
||||
use zebra_state::BoxError;
|
||||
|
@ -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)]
|
||||
|
|
|
@ -5,8 +5,11 @@ use std::sync::Arc;
|
|||
use tower::buffer::Buffer;
|
||||
|
||||
use zebra_chain::{
|
||||
block::Block, chain_tip::NoChainTip, parameters::Network::*,
|
||||
serialization::ZcashDeserializeInto,
|
||||
block::Block,
|
||||
chain_tip::NoChainTip,
|
||||
parameters::Network::*,
|
||||
serialization::{ZcashDeserializeInto, ZcashSerialize},
|
||||
transaction::{UnminedTx, UnminedTxId},
|
||||
};
|
||||
use zebra_network::constants::USER_AGENT;
|
||||
use zebra_node_services::BoxError;
|
||||
|
@ -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.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>>),
|
||||
|
||||
/// 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`],
|
||||
/// 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.
|
||||
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -111,7 +111,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)> {
|
||||
self.transaction_location(hash)
|
||||
.map(|TransactionLocation { index, height }| {
|
||||
let block = self
|
||||
|
@ -119,7 +122,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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use zebra_chain::{
|
||||
block::Block,
|
||||
block::{self, Block},
|
||||
transaction::{self, Transaction},
|
||||
};
|
||||
|
||||
|
@ -51,7 +51,7 @@ pub(crate) fn transaction<C>(
|
|||
chain: Option<C>,
|
||||
db: &ZebraDb,
|
||||
hash: transaction::Hash,
|
||||
) -> Option<Arc<Transaction>>
|
||||
) -> Option<(Arc<Transaction>, block::Height)>
|
||||
where
|
||||
C: AsRef<Chain>,
|
||||
{
|
||||
|
@ -65,6 +65,11 @@ where
|
|||
// (`chain` is always in memory, but `db` stores transactions on disk, with a memory cache.)
|
||||
chain
|
||||
.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))
|
||||
}
|
||||
|
|
|
@ -68,7 +68,10 @@ async fn populated_read_state_responds_correctly() -> Result<()> {
|
|||
for transaction in &block.transactions {
|
||||
let transaction_cases = vec![(
|
||||
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);
|
||||
|
|
Loading…
Reference in New Issue