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 3 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
89 changes: 67 additions & 22 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,7 @@ where
None
};

let self_clone = self.clone();
async move {
let hash_or_height: HashOrHeight = hash_or_height.parse().map_server_error()?;

Expand Down Expand Up @@ -835,15 +836,39 @@ where
}

let tx_ids_response = futs.next().await.expect("`futs` should not be empty");
let tx = match tx_ids_response.map_server_error()? {
let tx: Vec<_> = match tx_ids_response.map_server_error()? {
zebra_state::ReadResponse::TransactionIdsForBlock(tx_ids) => tx_ids
.ok_or_server_error("Block not found")?
.iter()
.map(|tx_id| tx_id.encode_hex())
.map(|tx_id| GetBlockTransaction::Hash(*tx_id))
.collect(),
_ => unreachable!("unmatched response to a transaction_ids_for_block request"),
};

let tx = if verbosity >= 2 {
let mut tx_obj_vec = vec![];
let mut tx_futs = FuturesOrdered::new();
let tx_len = tx.len();
for txid in tx {
let GetBlockTransaction::Hash(txid) = txid else {
unreachable!("must be a Hash")
};
tx_futs
.push_back(self_clone.get_raw_transaction(txid.encode_hex(), Some(1)));
}
for _ in 0..tx_len {
let get_tx_result =
tx_futs.next().await.expect("`tx_futs` should not be empty");
let GetRawTransaction::Object(tx_obj) = get_tx_result? else {
unreachable!("must return Object");
};
tx_obj_vec.push(GetBlockTransaction::Object(tx_obj));
}
tx_obj_vec
} else {
tx
};

let orchard_tree_response = futs.next().await.expect("`futs` should not be empty");
let zebra_state::ReadResponse::OrchardTree(orchard_tree) =
orchard_tree_response.map_server_error()?
Expand Down Expand Up @@ -1771,11 +1796,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 @@ -1803,7 +1826,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 @@ -1844,6 +1867,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 @@ -1987,22 +2021,33 @@ 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 transaction, or -1 if
/// the transaction is in the mempool.
height: i32,
/// The confirmations of the block in the best chain that contains the transaction,
/// or 0 if the transaction is in the mempool.
confirmations: u32,
},
Object(TransactionObject),
}

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

/// A Transaction object as returnedb by `getrawtransaction` RPC request.
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
#[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 transaction, or -1 if
/// the transaction is in the mempool.
pub height: i32,
/// The confirmations of the block in the best chain that contains the transaction,
/// or 0 if the transaction is in the mempool.
pub confirmations: 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 Down Expand Up @@ -2080,7 +2125,7 @@ impl GetRawTransaction {
verbose: bool,
) -> Self {
if verbose {
GetRawTransaction::Object {
GetRawTransaction::Object(TransactionObject {
hex: tx.into(),
height: match height {
Some(height) => height
Expand All @@ -2090,7 +2135,7 @@ impl GetRawTransaction {
None => -1,
},
confirmations,
}
})
} else {
GetRawTransaction::Raw(tx.into())
}
Expand Down
59 changes: 43 additions & 16 deletions zebra-rpc/src/methods/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,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 @@ -219,7 +219,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 All @@ -242,12 +242,31 @@ async fn rpc_getblock() {
);
}

// With verbosity=2, the RPC calls getrawtransaction which queries the
// mempool, which we need to mock since we used a MockService for it. This
// function returns a future that mocks that query. This is similar to what
// we use in the getrawtransaction test, but here we don't bother checking
// if the request is correct.
let make_mempool_req = || {
let mut mempool = mempool.clone();
async move {
mempool
.expect_request_that(|_request| true)
.await
.respond(mempool::Response::Transactions(vec![]));
}
};

// Make height calls with verbosity=2 and check response
for (i, block) in blocks.iter().enumerate() {
let get_block = rpc
.get_block(i.to_string(), Some(2u8))
.await
.expect("We should have a GetBlock struct");
let get_block_req = rpc.get_block(i.to_string(), Some(2u8));

// Run both the getblock request and the mocked mempool request.
// This assumes a single mempool query, i.e. that there is a single
// transaction the block. If the test vectors changes and the block
// has more than one transaction, this will need to be adjusted.
let (response, _) = futures::join!(get_block_req, make_mempool_req());
let get_block = response.expect("We should have a GetBlock struct");

let (expected_nonce, expected_final_sapling_root) =
get_block_data(&read_state, block.clone(), i).await;
Expand All @@ -262,7 +281,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: i.try_into().expect("valid u32"),
confirmations: (blocks.len() - i).try_into().expect("valid i64")
}))
.collect(),
trees,
size: None,
Expand All @@ -287,10 +310,10 @@ async fn rpc_getblock() {

// Make hash calls with verbosity=2 and check response
for (i, block) in blocks.iter().enumerate() {
let get_block = rpc
.get_block(blocks[i].hash().to_string(), Some(2u8))
.await
.expect("We should have a GetBlock struct");
let get_block_req = rpc.get_block(blocks[i].hash().to_string(), Some(2u8));

let (response, _) = futures::join!(get_block_req, make_mempool_req());
let get_block = response.expect("We should have a GetBlock struct");

let (expected_nonce, expected_final_sapling_root) =
get_block_data(&read_state, block.clone(), i).await;
Expand All @@ -305,7 +328,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: i.try_into().expect("valid u32"),
confirmations: (blocks.len() - i).try_into().expect("valid i64")
}))
.collect(),
trees,
size: None,
Expand Down Expand Up @@ -348,7 +375,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 @@ -391,7 +418,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 @@ -766,11 +793,11 @@ async fn rpc_getrawtransaction() {
}

let (response, _) = futures::join!(get_tx_verbose_1_req, make_mempool_req(tx_hash));
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
6 changes: 3 additions & 3 deletions zebra-rpc/src/tests/vectors.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Fixed Zebra RPC serialization test vectors.

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

#[test]
pub fn test_transaction_serialization() {
Expand All @@ -10,11 +10,11 @@ pub fn test_transaction_serialization() {

assert_eq!(j, expected_json);

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

Expand Down
Loading