1. fix(rpc): Fix slow getblock RPC (verbose=1) using transaction ID index (#5307)

* Add RPC timing to zcash-rpc-diff

* Use transaction hash index for verbose block requests, rather than block data
This commit is contained in:
teor 2022-10-03 09:34:44 +10:00 committed by GitHub
parent 75a679792b
commit 211dbb437b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 183 additions and 37 deletions

View File

@ -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(),
}),
_ => Err(Error {
code: ErrorCode::InvalidParams,
message: "Invalid verbosity value".to_string(),
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,
}),
},
zebra_state::ReadResponse::Block(None) => Err(Error {
code: MISSING_BLOCK_ERROR_CODE,
message: "Block not found".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,
}),
_ => 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<String>,
},
}

View File

@ -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",

View File

@ -67,6 +67,11 @@ pub enum ReadResponse {
/// Response to [`ReadRequest::Transaction`] with the specified transaction.
Transaction(Option<(Arc<Transaction>, 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<Arc<[transaction::Hash]>>),
/// Response to [`ReadRequest::BlockLocator`] with a block locator object.
BlockLocator(Vec<block::Hash>),
@ -130,7 +135,8 @@ impl TryFrom<ReadResponse> 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(_)

View File

@ -1173,7 +1173,7 @@ impl Service<ReadRequest> 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<ReadRequest> 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();

View File

@ -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<Arc<[transaction::Hash]>> {
// 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.

View File

@ -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<Arc<[transaction::Hash]>> {
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<block::Hash> {
let hash = self.blocks.get(&height)?.hash;

View File

@ -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,

View File

@ -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<C>(
chain: Option<C>,
db: &ZebraDb,
hash_or_height: HashOrHeight,
) -> Option<Arc<[transaction::Hash]>>
where
C: AsRef<Chain>,
{
// # 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`.
///

View File

@ -89,10 +89,12 @@ echo "$@"
echo
echo "Querying $ZEBRAD $ZEBRAD_NET chain at height >=$ZEBRAD_HEIGHT..."
$ZCASH_CLI -rpcport="$ZEBRAD_RPC_PORT" "$@" > "$ZEBRAD_RESPONSE"
time $ZCASH_CLI -rpcport="$ZEBRAD_RPC_PORT" "$@" > "$ZEBRAD_RESPONSE"
echo
echo "Querying $ZCASHD $ZCASHD_NET chain at height >=$ZCASHD_HEIGHT..."
$ZCASH_CLI "$@" > "$ZCASHD_RESPONSE"
time $ZCASH_CLI "$@" > "$ZCASHD_RESPONSE"
echo
echo