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

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

Merged
merged 2 commits into from
Oct 2, 2022
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
81 changes: 50 additions & 31 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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>,
},
}
Expand Down
13 changes: 13 additions & 0 deletions zebra-state/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 7 additions & 1 deletion zebra-state/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>),

Expand Down Expand Up @@ -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(_)
Expand Down
35 changes: 34 additions & 1 deletion zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand Down
33 changes: 33 additions & 0 deletions zebra-state/src/service/finalized_state/zebra_db/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions zebra-state/src/service/non_finalized_state/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion zebra-state/src/service/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions zebra-state/src/service/read/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
///
Expand Down
6 changes: 4 additions & 2 deletions zebra-utils/zcash-rpc-diff
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down