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

add(rpc): getblock: return transaction details with verbosity=2 #9083

Merged
merged 6 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
141 changes: 98 additions & 43 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! Some parts of the `zcashd` RPC documentation are outdated.
//! So this implementation follows the `zcashd` server and `lightwalletd` client implementations.

use std::{collections::HashSet, fmt::Debug};
use std::{collections::HashSet, fmt::Debug, sync::Arc};

use chrono::Utc;
use futures::{stream::FuturesOrdered, FutureExt, StreamExt, TryFutureExt};
Expand Down Expand Up @@ -814,6 +814,12 @@ where
next_block_hash,
} = *block_header;

let transactions_request = match verbosity {
1 => zebra_state::ReadRequest::TransactionIdsForBlock(hash_or_height),
2 => zebra_state::ReadRequest::Block(hash_or_height),
_other => panic!("get_block_header_fut should be none"),
};

// # Concurrency
//
// We look up by block hash so the hash, transaction IDs, and confirmations
Expand All @@ -827,7 +833,7 @@ where
// A block's transaction IDs are never modified, so all possible responses are
// valid. Clients that query block heights must be able to handle chain forks,
// including getting transaction IDs from any chain fork.
zebra_state::ReadRequest::TransactionIdsForBlock(hash_or_height),
transactions_request,
// Orchard trees
zebra_state::ReadRequest::OrchardTree(hash_or_height),
];
Expand All @@ -839,11 +845,27 @@ where
}

let tx_ids_response = futs.next().await.expect("`futs` should not be empty");
let tx = match tx_ids_response.map_misc_error()? {
let tx: Vec<_> = match tx_ids_response.map_misc_error()? {
zebra_state::ReadResponse::TransactionIdsForBlock(tx_ids) => tx_ids
.ok_or_misc_error("block not found")?
.iter()
.map(|tx_id| tx_id.encode_hex())
.map(|tx_id| GetBlockTransaction::Hash(*tx_id))
.collect(),
zebra_state::ReadResponse::Block(block) => block
.ok_or_misc_error("Block not found")?
.transactions
.iter()
.map(|tx| {
GetBlockTransaction::Object(TransactionObject::from_transaction(
tx.clone(),
Some(height),
Some(
confirmations
.try_into()
.expect("should be less than max block height, i32::MAX"),
),
))
})
.collect(),
_ => unreachable!("unmatched response to a transaction_ids_for_block request"),
};
Expand Down Expand Up @@ -1131,15 +1153,14 @@ where
{
mempool::Response::Transactions(txns) => {
if let Some(tx) = txns.first() {
let hex = tx.transaction.clone().into();

return Ok(if verbose {
GetRawTransaction::Object {
hex,
height: None,
confirmations: None,
}
GetRawTransaction::Object(TransactionObject::from_transaction(
tx.transaction.clone(),
None,
None,
))
} else {
let hex = tx.transaction.clone().into();
GetRawTransaction::Raw(hex)
});
}
Expand All @@ -1155,19 +1176,16 @@ where
.await
.map_misc_error()?
{
zebra_state::ReadResponse::Transaction(Some(tx)) => {
zebra_state::ReadResponse::Transaction(Some(tx)) => Ok(if verbose {
GetRawTransaction::Object(TransactionObject::from_transaction(
tx.tx.clone(),
Some(tx.height),
Some(tx.confirmations),
))
} else {
let hex = tx.tx.into();

Ok(if verbose {
GetRawTransaction::Object {
hex,
height: Some(tx.height.0),
confirmations: Some(tx.confirmations),
}
} else {
GetRawTransaction::Raw(hex)
})
}
GetRawTransaction::Raw(hex)
}),

zebra_state::ReadResponse::Transaction(None) => {
Err("No such mempool or main chain transaction")
Expand Down Expand Up @@ -1779,11 +1797,9 @@ pub enum GetBlock {

// `chainhistoryroot` would be here. Undocumented. TODO: decide if we want to support it
//
/// List of transaction IDs in block order, hex-encoded.
//
// TODO: use a typed Vec<transaction::Hash> here
// TODO: support Objects
tx: Vec<String>,
/// List of transactions in block order, hex-encoded if verbosity=1 or
/// as objects if verbosity=2.
tx: Vec<GetBlockTransaction>,

/// The height of the requested block.
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -1811,7 +1827,7 @@ pub enum GetBlock {
difficulty: Option<f64>,

// `chainwork` would be here, but we don't plan on supporting it
// `anchor` would be here. Undocumented. TODO: decide if we want to support it
// `anchor` would be here. Not planned to be supported.
// `chainSupply` would be here, TODO: implement
// `valuePools` would be here, TODO: implement
//
Expand Down Expand Up @@ -1852,6 +1868,17 @@ impl Default for GetBlock {
}
}

#[derive(Clone, Debug, PartialEq, serde::Serialize)]
#[serde(untagged)]
/// The transaction list in a `getblock` call. Can be a list of transaction
/// IDs or the full transaction details depending on verbosity.
pub enum GetBlockTransaction {
/// The transaction hash, hex-encoded.
Hash(#[serde(with = "hex")] transaction::Hash),
/// The block object.
Object(TransactionObject),
}

/// Response to a `getblockheader` RPC request.
///
/// See the notes for the [`Rpc::get_block_header`] method.
Expand Down Expand Up @@ -1995,24 +2022,36 @@ 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 in the best chain that contains the tx or `None` if the tx is in
/// the mempool.
#[serde(skip_serializing_if = "Option::is_none")]
height: Option<u32>,
/// The height diff between the block containing the tx and the best chain tip + 1 or `None`
/// if the tx is in the mempool.
#[serde(skip_serializing_if = "Option::is_none")]
confirmations: Option<u32>,
},
Object(TransactionObject),
}

impl Default for GetRawTransaction {
fn default() -> Self {
Self::Object {
Self::Object(TransactionObject::default())
}
}

/// A Transaction object as returned by `getrawtransaction` and `getblock` RPC
/// requests.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct TransactionObject {
/// The raw transaction, encoded as hex bytes.
#[serde(with = "hex")]
pub hex: SerializedTransaction,
/// The height of the block in the best chain that contains the tx or `None` if the tx is in
/// the mempool.
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
/// The height diff between the block containing the tx and the best chain tip + 1 or `None`
/// if the tx is in the mempool.
#[serde(skip_serializing_if = "Option::is_none")]
pub confirmations: Option<u32>,
// TODO: many fields not yet supported
}

impl Default for TransactionObject {
fn default() -> Self {
Self {
hex: SerializedTransaction::from(
[0u8; zebra_chain::transaction::MIN_TRANSPARENT_TX_SIZE as usize].to_vec(),
),
Expand All @@ -2022,6 +2061,22 @@ impl Default for GetRawTransaction {
}
}

impl TransactionObject {
/// Converts `tx` and `height` into a new `GetRawTransaction` in the `verbose` format.
#[allow(clippy::unwrap_in_result)]
fn from_transaction(
tx: Arc<Transaction>,
height: Option<block::Height>,
confirmations: Option<u32>,
) -> Self {
Self {
hex: tx.into(),
height: height.map(|height| height.0),
confirmations,
}
}
}

/// Response to a `getaddressutxos` RPC request.
///
/// See the notes for the [`Rpc::get_address_utxos` method].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ expression: block
"merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609",
"finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000",
"tx": [
"851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609"
{
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0250c30000000000002321027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875acd43000000000000017a9147d46a730d31f97b1930d3368a967c309bd4d136a8700000000",
"height": 1,
"confirmations": 10
}
],
"time": 1477671596,
"nonce": "9057977ea6d4ae867decc96359fcf2db8cdebcbfb3bd549de4f21f16cfe83475",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ expression: block
"merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75",
"finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000",
"tx": [
"f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75"
{
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0250c30000000000002321025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99acd43000000000000017a914ef775f1f997f122a062fff1a2d7443abd1f9c6428700000000",
"height": 1,
"confirmations": 10
}
],
"time": 1477674473,
"nonce": "0000e5739438a096ca89cde16bcf6001e0c5a7ce6f7c591d26314c26c2560000",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ expression: block
"merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609",
"finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000",
"tx": [
"851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609"
{
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0250c30000000000002321027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875acd43000000000000017a9147d46a730d31f97b1930d3368a967c309bd4d136a8700000000",
"height": 1,
"confirmations": 10
}
],
"time": 1477671596,
"nonce": "9057977ea6d4ae867decc96359fcf2db8cdebcbfb3bd549de4f21f16cfe83475",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ expression: block
"merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75",
"finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000",
"tx": [
"f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75"
{
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0250c30000000000002321025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99acd43000000000000017a914ef775f1f997f122a062fff1a2d7443abd1f9c6428700000000",
"height": 1,
"confirmations": 10
}
],
"time": 1477674473,
"nonce": "0000e5739438a096ca89cde16bcf6001e0c5a7ce6f7c591d26314c26c2560000",
Expand Down
24 changes: 16 additions & 8 deletions zebra-rpc/src/methods/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ async fn rpc_getblock() {
tx: block
.transactions
.iter()
.map(|tx| tx.hash().encode_hex())
.map(|tx| GetBlockTransaction::Hash(tx.hash()))
.collect(),
trees,
size: None,
Expand Down Expand Up @@ -221,7 +221,7 @@ async fn rpc_getblock() {
tx: block
.transactions
.iter()
.map(|tx| tx.hash().encode_hex())
.map(|tx| GetBlockTransaction::Hash(tx.hash()))
.collect(),
trees,
size: None,
Expand Down Expand Up @@ -264,7 +264,11 @@ async fn rpc_getblock() {
tx: block
.transactions
.iter()
.map(|tx| tx.hash().encode_hex())
.map(|tx| GetBlockTransaction::Object(TransactionObject {
hex: (*tx).clone().into(),
height: Some(i.try_into().expect("valid u32")),
confirmations: Some((blocks.len() - i).try_into().expect("valid i64"))
}))
.collect(),
trees,
size: None,
Expand Down Expand Up @@ -307,7 +311,11 @@ async fn rpc_getblock() {
tx: block
.transactions
.iter()
.map(|tx| tx.hash().encode_hex())
.map(|tx| GetBlockTransaction::Object(TransactionObject {
hex: (*tx).clone().into(),
height: Some(i.try_into().expect("valid u32")),
confirmations: Some((blocks.len() - i).try_into().expect("valid i64"))
}))
.collect(),
trees,
size: None,
Expand Down Expand Up @@ -350,7 +358,7 @@ async fn rpc_getblock() {
tx: block
.transactions
.iter()
.map(|tx| tx.hash().encode_hex())
.map(|tx| GetBlockTransaction::Hash(tx.hash()))
.collect(),
trees,
size: None,
Expand Down Expand Up @@ -393,7 +401,7 @@ async fn rpc_getblock() {
tx: block
.transactions
.iter()
.map(|tx| tx.hash().encode_hex())
.map(|tx| GetBlockTransaction::Hash(tx.hash()))
.collect(),
trees,
size: None,
Expand Down Expand Up @@ -774,11 +782,11 @@ async fn rpc_getrawtransaction() {

let (response, _) = futures::join!(get_tx_verbose_1_req, make_mempool_req(txid));

let GetRawTransaction::Object {
let GetRawTransaction::Object(TransactionObject {
hex,
height,
confirmations,
} = response.expect("We should have a GetRawTransaction struct")
}) = response.expect("We should have a GetRawTransaction struct")
else {
unreachable!("Should return a Raw enum")
};
Expand Down
10 changes: 5 additions & 5 deletions zebra-rpc/src/tests/vectors.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
//! Fixed Zebra RPC serialization test vectors.

use crate::methods::{GetBlock, GetRawTransaction};
use crate::methods::{GetBlock, GetRawTransaction, TransactionObject};

#[test]
pub fn test_transaction_serialization() {
let tx = GetRawTransaction::Raw(vec![0x42].into());

assert_eq!(serde_json::to_string(&tx).unwrap(), r#""42""#);

let tx = GetRawTransaction::Object {
let tx = GetRawTransaction::Object(TransactionObject {
hex: vec![0x42].into(),
height: Some(1),
confirmations: Some(0),
};
});

assert_eq!(
serde_json::to_string(&tx).unwrap(),
r#"{"hex":"42","height":1,"confirmations":0}"#
);

let tx = GetRawTransaction::Object {
let tx = GetRawTransaction::Object(TransactionObject {
hex: vec![0x42].into(),
height: None,
confirmations: None,
};
});

assert_eq!(serde_json::to_string(&tx).unwrap(), r#"{"hex":"42"}"#);
}
Expand Down
Loading