diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 4f9e4bfb40e..79f2d34f66e 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -540,46 +540,65 @@ where let mut state = self.state.clone(); async move { - let height = height.parse().map_err(|error: SerializationError| Error { + let height: Height = height.parse().map_err(|error: SerializationError| Error { code: ErrorCode::ServerError(0), message: error.to_string(), data: None, })?; - let request = - zebra_state::ReadRequest::Block(zebra_state::HashOrHeight::Height(height)); - 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, - })?; + if verbosity == 0 { + let request = zebra_state::ReadRequest::Block(height.into()); + 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::Block(Some(block)) => match verbosity { - 0 => Ok(GetBlock::Raw(block.into())), - 1 => Ok(GetBlock::Object { - tx: block - .transactions - .iter() - .map(|tx| tx.hash().encode_hex()) - .collect(), + match response { + zebra_state::ReadResponse::Block(Some(block)) => { + Ok(GetBlock::Raw(block.into())) + } + zebra_state::ReadResponse::Block(None) => Err(Error { + code: MISSING_BLOCK_ERROR_CODE, + message: "Block not found".to_string(), + data: None, }), - _ => Err(Error { - code: ErrorCode::InvalidParams, - message: "Invalid verbosity value".to_string(), + _ => unreachable!("unmatched response to a block request"), + } + } else if verbosity == 1 { + let request = zebra_state::ReadRequest::TransactionIdsForBlock(height.into()); + 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::TransactionIdsForBlock(Some(tx_ids)) => { + let tx_ids = tx_ids.iter().map(|tx_id| tx_id.encode_hex()).collect(); + Ok(GetBlock::Object { tx: tx_ids }) + } + zebra_state::ReadResponse::TransactionIdsForBlock(None) => Err(Error { + code: MISSING_BLOCK_ERROR_CODE, + message: "Block not found".to_string(), data: None, }), - }, - zebra_state::ReadResponse::Block(None) => Err(Error { - code: MISSING_BLOCK_ERROR_CODE, - message: "Block not found".to_string(), + _ => unreachable!("unmatched response to a transaction_ids_for_block request"), + } + } else { + Err(Error { + code: ErrorCode::InvalidParams, + message: "Invalid verbosity value".to_string(), data: None, - }), - _ => unreachable!("unmatched response to a block request"), + }) } } .boxed() @@ -1111,7 +1130,7 @@ pub enum GetBlock { Raw(#[serde(with = "hex")] SerializedBlock), /// The block object. Object { - /// Vector of hex-encoded TXIDs of the transactions of the block + /// List of transaction IDs in block order, hex-encoded. tx: Vec, }, } diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 044c6a9a9c0..bca3f01409b 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -597,6 +597,18 @@ pub enum ReadRequest { /// * [`ReadResponse::Transaction(None)`](ReadResponse::Transaction) otherwise. Transaction(transaction::Hash), + /// Looks up the transaction IDs for a block, using a block hash or height. + /// + /// Returns + /// + /// * An ordered list of transaction hashes, or + /// * `None` if the block was not found. + /// + /// Note: Each block has at least one transaction: the coinbase transaction. + /// + /// Returned txids are in the order they appear in the block. + TransactionIdsForBlock(HashOrHeight), + /// Looks up a UTXO identified by the given [`OutPoint`](transparent::OutPoint), /// returning `None` immediately if it is unknown. /// @@ -728,6 +740,7 @@ impl ReadRequest { ReadRequest::Depth(_) => "depth", ReadRequest::Block(_) => "block", ReadRequest::Transaction(_) => "transaction", + ReadRequest::TransactionIdsForBlock(_) => "transaction_ids_for_block", ReadRequest::BestChainUtxo { .. } => "best_chain_utxo", ReadRequest::AnyChainUtxo { .. } => "any_chain_utxo", ReadRequest::BlockLocator => "block_locator", diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index ca66938b20f..7ca08b3fd60 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -67,6 +67,11 @@ pub enum ReadResponse { /// Response to [`ReadRequest::Transaction`] with the specified transaction. Transaction(Option<(Arc, block::Height)>), + /// Response to [`ReadRequest::TransactionIdsForBlock`], + /// with an list of transaction hashes in block order, + /// or `None` if the block was not found. + TransactionIdsForBlock(Option>), + /// Response to [`ReadRequest::BlockLocator`] with a block locator object. BlockLocator(Vec), @@ -130,7 +135,8 @@ impl TryFrom for Response { ReadResponse::BlockHashes(hashes) => Ok(Response::BlockHashes(hashes)), ReadResponse::BlockHeaders(headers) => Ok(Response::BlockHeaders(headers)), - ReadResponse::BestChainUtxo(_) + ReadResponse::TransactionIdsForBlock(_) + | ReadResponse::BestChainUtxo(_) | ReadResponse::SaplingTree(_) | ReadResponse::OrchardTree(_) | ReadResponse::AddressBalance(_) diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index de881879171..b3ee4d392dd 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1173,7 +1173,7 @@ impl Service for ReadStateService { .boxed() } - // Used by get_block RPC and the StateService. + // Used by the get_block (raw) RPC and the StateService. ReadRequest::Block(hash_or_height) => { let timer = CodeTimer::start(); @@ -1227,6 +1227,39 @@ impl Service for ReadStateService { .boxed() } + // Used by the getblock (verbose) RPC. + ReadRequest::TransactionIdsForBlock(hash_or_height) => { + let timer = CodeTimer::start(); + + let state = self.clone(); + + let span = Span::current(); + tokio::task::spawn_blocking(move || { + span.in_scope(move || { + let transaction_ids = state.non_finalized_state_receiver.with_watch_data( + |non_finalized_state| { + read::transaction_hashes_for_block( + non_finalized_state.best_chain(), + &state.db, + hash_or_height, + ) + }, + ); + + // The work is done in the future. + timer.finish( + module_path!(), + line!(), + "ReadRequest::TransactionIdsForBlock", + ); + + Ok(ReadResponse::TransactionIdsForBlock(transaction_ids)) + }) + }) + .map(|join_result| join_result.expect("panic in ReadRequest::Block")) + .boxed() + } + // Currently unused. ReadRequest::BestChainUtxo(outpoint) => { let timer = CodeTimer::start(); diff --git a/zebra-state/src/service/finalized_state/zebra_db/block.rs b/zebra-state/src/service/finalized_state/zebra_db/block.rs index 6e092758ab5..cc707f51770 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block.rs @@ -226,6 +226,39 @@ impl ZebraDb { .map(|tx| (tx, transaction_location.height)) } + /// Returns the [`transaction::Hash`]es in the block with `hash_or_height`, + /// if it exists in this chain. + /// + /// Hashes are returned in block order. + /// + /// Returns `None` if the block is not found. + #[allow(clippy::unwrap_in_result)] + pub fn transaction_hashes_for_block( + &self, + hash_or_height: HashOrHeight, + ) -> Option> { + // Block + let height = hash_or_height.height_or_else(|hash| self.height(hash))?; + + // Transaction hashes + let hash_by_tx_loc = self.db.cf_handle("hash_by_tx_loc").unwrap(); + + // Manually fetch the entire block's transaction hashes + let mut transaction_hashes = Vec::new(); + + for tx_index in 0..=Transaction::max_allocation() { + let tx_loc = TransactionLocation::from_u64(height, tx_index); + + if let Some(tx_hash) = self.db.zs_get(&hash_by_tx_loc, &tx_loc) { + transaction_hashes.push(tx_hash); + } else { + break; + } + } + + Some(transaction_hashes.into()) + } + // Write block methods /// Write `finalized` to the finalized state. diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 0d707b197e0..1c91f9c42e3 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -471,6 +471,21 @@ impl Chain { .get(tx_loc.index.as_usize()) } + /// Returns the [`transaction::Hash`]es in the block with `hash_or_height`, + /// if it exists in this chain. + /// + /// Hashes are returned in block order. + /// + /// Returns `None` if the block is not found. + pub fn transaction_hashes_for_block( + &self, + hash_or_height: HashOrHeight, + ) -> Option> { + let transaction_hashes = self.block(hash_or_height)?.transaction_hashes.clone(); + + Some(transaction_hashes) + } + /// Returns the [`block::Hash`] for `height`, if it exists in this chain. pub fn hash_by_height(&self, height: Height) -> Option { let hash = self.blocks.get(&height)?.hash; diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index e2a0c01695d..d1f7f4c0a81 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -27,7 +27,7 @@ pub use address::{ tx_id::transparent_tx_ids, utxo::{address_utxos, AddressUtxos, ADDRESS_HEIGHTS_FULL_RANGE}, }; -pub use block::{any_utxo, block, block_header, transaction, utxo}; +pub use block::{any_utxo, block, block_header, transaction, transaction_hashes_for_block, utxo}; pub use find::{ block_locator, chain_contains_hash, depth, find_chain_hashes, find_chain_headers, hash_by_height, height_by_hash, tip, tip_height, diff --git a/zebra-state/src/service/read/block.rs b/zebra-state/src/service/read/block.rs index 985c44eaf94..54eb485c367 100644 --- a/zebra-state/src/service/read/block.rs +++ b/zebra-state/src/service/read/block.rs @@ -93,6 +93,31 @@ where .or_else(|| db.transaction(hash)) } +/// Returns the [`transaction::Hash`]es for the block with `hash_or_height`, +/// if it exists in the non-finalized `chain` or finalized `db`. +/// +/// The returned hashes are in block order. +/// +/// Returns `None` if the block is not found. +pub fn transaction_hashes_for_block( + chain: Option, + db: &ZebraDb, + hash_or_height: HashOrHeight, +) -> Option> +where + C: AsRef, +{ + // # Correctness + // + // Since blocks are the same in the finalized and non-finalized state, we + // check the most efficient alternative first. (`chain` is always in memory, + // but `db` stores blocks on disk, with a memory cache.) + chain + .as_ref() + .and_then(|chain| chain.as_ref().transaction_hashes_for_block(hash_or_height)) + .or_else(|| db.transaction_hashes_for_block(hash_or_height)) +} + /// Returns the [`Utxo`] for [`transparent::OutPoint`], if it exists in the /// non-finalized `chain` or finalized `db`. ///