change(rpc): Add value pool balances to `getblockchaininfo` RPC method response (#8769)

* Moves `Zec` type out from behind feature flag

* Adds 'ValuePoolBalance` type

* Updates getblockchaininfo return type to a BoxFuture

* minor refactor

* Adds service request

* Adds real value balances to getblockchaininfo RPC response

* Updates snapshots and the suggested command for updating snapshots

* Uses generic error constructors wherever possible and removes outdated TODOs

* Updates prop tests to handle mock service requests
This commit is contained in:
Arya 2024-08-16 15:54:44 -04:00 committed by GitHub
parent 11b0833374
commit 83c6725e84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 473 additions and 158 deletions

View File

@ -14,7 +14,7 @@ pub mod mock;
#[cfg(test)]
mod tests;
use network_chain_tip_height_estimator::NetworkChainTipHeightEstimator;
pub use network_chain_tip_height_estimator::NetworkChainTipHeightEstimator;
/// An interface for querying the chain tip.
///

View File

@ -21,7 +21,7 @@ use tracing::Instrument;
use zcash_primitives::consensus::Parameters;
use zebra_chain::{
block::{self, Height, SerializedBlock},
chain_tip::ChainTip,
chain_tip::{ChainTip, NetworkChainTipHeightEstimator},
parameters::{ConsensusBranchId, Network, NetworkUpgrade},
serialization::ZcashDeserialize,
subtree::NoteCommitmentSubtreeIndex,
@ -45,6 +45,8 @@ use errors::{MapServerError, OkOrServerError};
// We don't use a types/ module here, because it is redundant.
pub mod trees;
pub mod types;
#[cfg(feature = "getblocktemplate-rpcs")]
pub mod get_block_template_rpcs;
@ -85,7 +87,7 @@ pub trait Rpc {
/// Some fields from the zcashd reference are missing from Zebra's [`GetBlockChainInfo`]. It only contains the fields
/// [required for lightwalletd support.](https://github.com/zcash/lightwalletd/blob/v0.4.9/common/common.go#L72-L89)
#[rpc(name = "getblockchaininfo")]
fn get_blockchain_info(&self) -> Result<GetBlockChainInfo>;
fn get_blockchain_info(&self) -> BoxFuture<Result<GetBlockChainInfo>>;
/// Returns the total balance of a provided `addresses` in an [`AddressBalance`] instance.
///
@ -500,98 +502,120 @@ where
Ok(response)
}
// TODO: use a generic error constructor (#5548)
#[allow(clippy::unwrap_in_result)]
fn get_blockchain_info(&self) -> Result<GetBlockChainInfo> {
let network = &self.network;
fn get_blockchain_info(&self) -> BoxFuture<Result<GetBlockChainInfo>> {
let network = self.network.clone();
let debug_force_finished_sync = self.debug_force_finished_sync;
let mut state = self.state.clone();
// `chain` field
let chain = self.network.bip70_network_name();
async move {
// `chain` field
let chain = network.bip70_network_name();
// `blocks` and `best_block_hash` fields
let (tip_height, tip_hash) = self
.latest_chain_tip
.best_tip_height_and_hash()
.ok_or_server_error("No Chain tip available yet")?;
let request = zebra_state::ReadRequest::TipPoolValues;
let response: zebra_state::ReadResponse = state
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
// `estimated_height` field
let current_block_time = self
.latest_chain_tip
.best_tip_block_time()
.ok_or_server_error("No Chain tip available yet")?;
let zebra_state::ReadResponse::TipPoolValues {
tip_height,
tip_hash,
value_balance,
} = response
else {
unreachable!("unmatched response to a TipPoolValues request")
};
let zebra_estimated_height = self
.latest_chain_tip
.estimate_network_chain_tip_height(network, Utc::now())
.ok_or_server_error("No Chain tip available yet")?;
let request = zebra_state::ReadRequest::BlockHeader(tip_hash.into());
let response: zebra_state::ReadResponse = state
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
let mut estimated_height =
if current_block_time > Utc::now() || zebra_estimated_height < tip_height {
let zebra_state::ReadResponse::BlockHeader(block_header) = response else {
unreachable!("unmatched response to a BlockHeader request")
};
let tip_block_time = block_header
.ok_or_server_error("unexpectedly could not read best chain tip block header")?
.time;
let now = Utc::now();
let zebra_estimated_height =
NetworkChainTipHeightEstimator::new(tip_block_time, tip_height, &network)
.estimate_height_at(now);
// If we're testing the mempool, force the estimated height to be the actual tip height, otherwise,
// check if the estimated height is below Zebra's latest tip height, or if the latest tip's block time is
// later than the current time on the local clock.
let estimated_height = if tip_block_time > now
|| zebra_estimated_height < tip_height
|| debug_force_finished_sync
{
tip_height
} else {
zebra_estimated_height
};
// If we're testing the mempool, force the estimated height to be the actual tip height.
if self.debug_force_finished_sync {
estimated_height = tip_height;
}
// `upgrades` object
//
// Get the network upgrades in height order, like `zcashd`.
let mut upgrades = IndexMap::new();
for (activation_height, network_upgrade) in network.full_activation_list() {
// Zebra defines network upgrades based on incompatible consensus rule changes,
// but zcashd defines them based on ZIPs.
// `upgrades` object
//
// All the network upgrades with a consensus branch ID are the same in Zebra and zcashd.
if let Some(branch_id) = network_upgrade.branch_id() {
// zcashd's RPC seems to ignore Disabled network upgrades, so Zebra does too.
let status = if tip_height >= activation_height {
NetworkUpgradeStatus::Active
} else {
NetworkUpgradeStatus::Pending
};
// Get the network upgrades in height order, like `zcashd`.
let mut upgrades = IndexMap::new();
for (activation_height, network_upgrade) in network.full_activation_list() {
// Zebra defines network upgrades based on incompatible consensus rule changes,
// but zcashd defines them based on ZIPs.
//
// All the network upgrades with a consensus branch ID are the same in Zebra and zcashd.
if let Some(branch_id) = network_upgrade.branch_id() {
// zcashd's RPC seems to ignore Disabled network upgrades, so Zebra does too.
let status = if tip_height >= activation_height {
NetworkUpgradeStatus::Active
} else {
NetworkUpgradeStatus::Pending
};
let upgrade = NetworkUpgradeInfo {
name: network_upgrade,
activation_height,
status,
};
upgrades.insert(ConsensusBranchIdHex(branch_id), upgrade);
let upgrade = NetworkUpgradeInfo {
name: network_upgrade,
activation_height,
status,
};
upgrades.insert(ConsensusBranchIdHex(branch_id), upgrade);
}
}
// `consensus` object
let next_block_height =
(tip_height + 1).expect("valid chain tips are a lot less than Height::MAX");
let consensus = TipConsensusBranch {
chain_tip: ConsensusBranchIdHex(
NetworkUpgrade::current(&network, tip_height)
.branch_id()
.unwrap_or(ConsensusBranchId::RPC_MISSING_ID),
),
next_block: ConsensusBranchIdHex(
NetworkUpgrade::current(&network, next_block_height)
.branch_id()
.unwrap_or(ConsensusBranchId::RPC_MISSING_ID),
),
};
let response = GetBlockChainInfo {
chain,
blocks: tip_height,
best_block_hash: tip_hash,
estimated_height,
value_pools: types::ValuePoolBalance::from_value_balance(value_balance),
upgrades,
consensus,
};
Ok(response)
}
// `consensus` object
let next_block_height =
(tip_height + 1).expect("valid chain tips are a lot less than Height::MAX");
let consensus = TipConsensusBranch {
chain_tip: ConsensusBranchIdHex(
NetworkUpgrade::current(network, tip_height)
.branch_id()
.unwrap_or(ConsensusBranchId::RPC_MISSING_ID),
),
next_block: ConsensusBranchIdHex(
NetworkUpgrade::current(network, next_block_height)
.branch_id()
.unwrap_or(ConsensusBranchId::RPC_MISSING_ID),
),
};
let response = GetBlockChainInfo {
chain,
blocks: tip_height,
best_block_hash: tip_hash,
estimated_height,
upgrades,
consensus,
};
Ok(response)
.boxed()
}
// TODO: use a generic error constructor (#5548)
fn get_address_balance(
&self,
address_strings: AddressStrings,
@ -615,7 +639,6 @@ where
}
// TODO: use HexData or GetRawTransaction::Bytes to handle the transaction data argument
// use a generic error constructor (#5548)
fn send_raw_transaction(
&self,
raw_transaction_hex: String,
@ -963,7 +986,6 @@ where
}
// TODO: use HexData or SentTransactionHash to handle the transaction ID
// use a generic error constructor (#5548)
fn get_raw_transaction(
&self,
txid_hex: String,
@ -1197,7 +1219,6 @@ where
.boxed()
}
// TODO: use a generic error constructor (#5548)
fn get_address_tx_ids(
&self,
request: GetAddressTxIdsRequest,
@ -1258,7 +1279,6 @@ where
.boxed()
}
// TODO: use a generic error constructor (#5548)
fn get_address_utxos(
&self,
address_strings: AddressStrings,
@ -1372,6 +1392,10 @@ pub struct GetBlockChainInfo {
#[serde(rename = "estimatedheight")]
estimated_height: Height,
/// Value pool balances
#[serde(rename = "valuePools")]
value_pools: [types::ValuePoolBalance; 5],
/// Status of network upgrades
upgrades: IndexMap<ConsensusBranchIdHex, NetworkUpgradeInfo>,
@ -1386,6 +1410,7 @@ impl Default for GetBlockChainInfo {
blocks: Height(1),
best_block_hash: block::Hash([0; 32]),
estimated_height: Height(1),
value_pools: types::ValuePoolBalance::zero_pools(),
upgrades: IndexMap::new(),
consensus: TipConsensusBranch {
chain_tip: ConsensusBranchIdHex(ConsensusBranchId::default()),

View File

@ -551,7 +551,6 @@ where
best_chain_tip_height(&self.latest_chain_tip).map(|height| height.0)
}
// TODO: use a generic error constructor (#5548)
fn get_block_hash(&self, index: i32) -> BoxFuture<Result<GetBlockHash>> {
let mut state = self.state.clone();
let latest_chain_tip = self.latest_chain_tip.clone();
@ -567,11 +566,7 @@ where
.ready()
.and_then(|service| service.call(request))
.await
.map_err(|error| Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
})?;
.map_server_error()?;
match response {
zebra_state::ReadResponse::BlockHash(Some(hash)) => Ok(GetBlockHash(hash)),
@ -586,7 +581,6 @@ where
.boxed()
}
// TODO: use a generic error constructor (#5548)
fn get_block_template(
&self,
parameters: Option<get_block_template::JsonParameters>,
@ -830,11 +824,7 @@ where
Is Zebra shutting down?"
);
return Err(Error {
code: ErrorCode::ServerError(0),
message: recv_error.to_string(),
data: None,
});
return Err(recv_error).map_server_error();
}
}
}

View File

@ -11,4 +11,3 @@ pub mod transaction;
pub mod unified_address;
pub mod validate_address;
pub mod z_validate_address;
pub mod zec;

View File

@ -6,7 +6,7 @@ use zebra_chain::{
transparent,
};
use crate::methods::get_block_template_rpcs::types::zec::Zec;
use crate::methods::types::Zec;
/// A response to a `getblocksubsidy` RPC request
#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]

View File

@ -1,6 +1,6 @@
//! Randomised property tests for RPC methods.
use std::collections::HashSet;
use std::{collections::HashSet, sync::Arc};
use futures::{join, FutureExt, TryFutureExt};
use hex::ToHex;
@ -11,7 +11,7 @@ use tower::buffer::Buffer;
use zebra_chain::{
amount::{Amount, NonNegative},
block::{Block, Height},
block::{self, Block, Height},
chain_tip::{mock::MockChainTip, NoChainTip},
parameters::{
Network::{self, *},
@ -20,6 +20,7 @@ use zebra_chain::{
serialization::{ZcashDeserialize, ZcashSerialize},
transaction::{self, Transaction, UnminedTx, VerifiedUnminedTx},
transparent,
value_balance::ValueBalance,
};
use zebra_node_services::mempool;
use zebra_state::BoxError;
@ -553,19 +554,34 @@ proptest! {
NoChainTip,
);
let response = rpc.get_blockchain_info();
prop_assert_eq!(
&response.err().unwrap().message,
"No Chain tip available yet"
);
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
runtime.block_on(async move {
let response_fut = rpc.get_blockchain_info();
let mock_state_handler = {
let mut state = state.clone();
async move {
state
.expect_request(zebra_state::ReadRequest::TipPoolValues)
.await
.expect("getblockchaininfo should call mock state service with correct request")
.respond(Err(BoxError::from("no chain tip available yet")));
}
};
let (response, _) = tokio::join!(response_fut, mock_state_handler);
prop_assert_eq!(
&response.err().unwrap().message,
"no chain tip available yet"
);
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
Ok::<_, TestCaseError>(())
})?;
}
@ -581,17 +597,11 @@ proptest! {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
// get block data
// get arbitrary chain tip data
let block_height = block.coinbase_height().unwrap();
let block_hash = block.hash();
let block_time = block.header.time;
// create a mocked `ChainTip`
let (chain_tip, mock_chain_tip_sender) = MockChainTip::new();
mock_chain_tip_sender.send_best_tip_height(block_height);
mock_chain_tip_sender.send_best_tip_hash(block_hash);
mock_chain_tip_sender.send_best_tip_block_time(block_time);
// Start RPC with the mocked `ChainTip`
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
@ -601,50 +611,82 @@ proptest! {
true,
mempool.clone(),
Buffer::new(state.clone(), 1),
chain_tip,
NoChainTip,
);
let response = rpc.get_blockchain_info();
// Check response
match response {
Ok(info) => {
prop_assert_eq!(info.chain, network.bip70_network_name());
prop_assert_eq!(info.blocks, block_height);
prop_assert_eq!(info.best_block_hash, block_hash);
prop_assert!(info.estimated_height < Height::MAX);
prop_assert_eq!(
info.consensus.chain_tip.0,
NetworkUpgrade::current(&network, block_height)
.branch_id()
.unwrap()
);
prop_assert_eq!(
info.consensus.next_block.0,
NetworkUpgrade::current(&network, (block_height + 1).unwrap())
.branch_id()
.unwrap()
);
for u in info.upgrades {
let mut status = NetworkUpgradeStatus::Active;
if block_height < u.1.activation_height {
status = NetworkUpgradeStatus::Pending;
}
prop_assert_eq!(u.1.status, status);
}
}
Err(_) => {
unreachable!("Test should never error with the data we are feeding it")
}
};
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
// check no requests were made during this test
runtime.block_on(async move {
let response_fut = rpc.get_blockchain_info();
let mock_state_handler = {
let mut state = state.clone();
async move {
state
.expect_request(zebra_state::ReadRequest::TipPoolValues)
.await
.expect("getblockchaininfo should call mock state service with correct request")
.respond(zebra_state::ReadResponse::TipPoolValues {
tip_height: block_height,
tip_hash: block_hash,
value_balance: ValueBalance::default(),
});
state
.expect_request(zebra_state::ReadRequest::BlockHeader(block_hash.into()))
.await
.expect("getblockchaininfo should call mock state service with correct request")
.respond(zebra_state::ReadResponse::BlockHeader(Some(Arc::new(block::Header {
time: block_time,
version: Default::default(),
previous_block_hash: Default::default(),
merkle_root: Default::default(),
commitment_bytes: Default::default(),
difficulty_threshold: Default::default(),
nonce: Default::default(),
solution: Default::default()
}))));
}
};
let (response, _) = tokio::join!(response_fut, mock_state_handler);
// Check response
match response {
Ok(info) => {
prop_assert_eq!(info.chain, network.bip70_network_name());
prop_assert_eq!(info.blocks, block_height);
prop_assert_eq!(info.best_block_hash, block_hash);
prop_assert!(info.estimated_height < Height::MAX);
prop_assert_eq!(
info.consensus.chain_tip.0,
NetworkUpgrade::current(&network, block_height)
.branch_id()
.unwrap()
);
prop_assert_eq!(
info.consensus.next_block.0,
NetworkUpgrade::current(&network, (block_height + 1).unwrap())
.branch_id()
.unwrap()
);
for u in info.upgrades {
let mut status = NetworkUpgradeStatus::Active;
if block_height < u.1.activation_height {
status = NetworkUpgradeStatus::Pending;
}
prop_assert_eq!(u.1.status, status);
}
}
Err(_) => {
unreachable!("Test should never error with the data we are feeding it")
}
};
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;

View File

@ -2,7 +2,7 @@
//!
//! To update these snapshots, run:
//! ```sh
//! cargo insta test --review -p zebra-rpc --lib -- test_rpc_response_data
//! cargo insta test --review --release -p zebra-rpc --lib -- test_rpc_response_data
//! ```
use std::collections::BTreeMap;
@ -210,6 +210,7 @@ async fn test_rpc_response_data_for_network(network: &Network) {
if network.is_a_test_network() && !network.is_default_testnet() {
let get_blockchain_info = rpc
.get_blockchain_info()
.await
.expect("We should have a GetBlockChainInfo struct");
snapshot_rpc_getblockchaininfo("_future_nu6_height", get_blockchain_info, &settings);
@ -223,6 +224,7 @@ async fn test_rpc_response_data_for_network(network: &Network) {
// `getblockchaininfo`
let get_blockchain_info = rpc
.get_blockchain_info()
.await
.expect("We should have a GetBlockChainInfo struct");
snapshot_rpc_getblockchaininfo("", get_blockchain_info, &settings);

View File

@ -7,6 +7,33 @@ expression: info
"blocks": 10,
"bestblockhash": "00074c46a4aa8172df8ae2ad1848a2e084e1b6989b7d9e6132adc938bf835b36",
"estimatedheight": "[Height]",
"valuePools": [
{
"id": "transparent",
"chainValue": 0.034375,
"chainValueZat": 3437500
},
{
"id": "sprout",
"chainValue": 0.0,
"chainValueZat": 0
},
{
"id": "sapling",
"chainValue": 0.0,
"chainValueZat": 0
},
{
"id": "orchard",
"chainValue": 0.0,
"chainValueZat": 0
},
{
"id": "deferred",
"chainValue": 0.0,
"chainValueZat": 0
}
],
"upgrades": {
"5ba81b19": {
"name": "Overwinter",

View File

@ -7,6 +7,33 @@ expression: info
"blocks": 10,
"bestblockhash": "079f4c752729be63e6341ee9bce42fbbe37236aba22e3deb82405f3c2805c112",
"estimatedheight": "[Height]",
"valuePools": [
{
"id": "transparent",
"chainValue": 0.034375,
"chainValueZat": 3437500
},
{
"id": "sprout",
"chainValue": 0.0,
"chainValueZat": 0
},
{
"id": "sapling",
"chainValue": 0.0,
"chainValueZat": 0
},
{
"id": "orchard",
"chainValue": 0.0,
"chainValueZat": 0
},
{
"id": "deferred",
"chainValue": 0.0,
"chainValueZat": 0
}
],
"upgrades": {
"5ba81b19": {
"name": "Overwinter",

View File

@ -7,6 +7,33 @@ expression: info
"blocks": 10,
"bestblockhash": "079f4c752729be63e6341ee9bce42fbbe37236aba22e3deb82405f3c2805c112",
"estimatedheight": "[Height]",
"valuePools": [
{
"id": "transparent",
"chainValue": 0.034375,
"chainValueZat": 3437500
},
{
"id": "sprout",
"chainValue": 0.0,
"chainValueZat": 0
},
{
"id": "sapling",
"chainValue": 0.0,
"chainValueZat": 0
},
{
"id": "orchard",
"chainValue": 0.0,
"chainValueZat": 0
},
{
"id": "deferred",
"chainValue": 0.0,
"chainValueZat": 0
}
],
"upgrades": {
"5ba81b19": {
"name": "Overwinter",

View File

@ -0,0 +1,7 @@
//! Types used in RPC methods.
mod get_blockchain_info;
mod zec;
pub use get_blockchain_info::ValuePoolBalance;
pub use zec::Zec;

View File

@ -0,0 +1,72 @@
//! Types used in `getblockchaininfo` RPC method.
use zebra_chain::{
amount::{Amount, NonNegative},
value_balance::ValueBalance,
};
use super::*;
/// A value pool's balance in Zec and Zatoshis
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValuePoolBalance {
/// Name of the pool
id: String,
/// Total amount in the pool, in ZEC
chain_value: Zec<NonNegative>,
/// Total amount in the pool, in zatoshis
chain_value_zat: Amount<NonNegative>,
}
impl ValuePoolBalance {
/// Returns a list of [`ValuePoolBalance`]s converted from the default [`ValueBalance`].
pub fn zero_pools() -> [Self; 5] {
Self::from_value_balance(Default::default())
}
/// Creates a new [`ValuePoolBalance`] from a pool name and its value balance.
pub fn new(id: impl ToString, amount: Amount<NonNegative>) -> Self {
Self {
id: id.to_string(),
chain_value: Zec::from(amount),
chain_value_zat: amount,
}
}
/// Creates a [`ValuePoolBalance`] for the transparent pool.
pub fn transparent(amount: Amount<NonNegative>) -> Self {
Self::new("transparent", amount)
}
/// Creates a [`ValuePoolBalance`] for the Sprout pool.
pub fn sprout(amount: Amount<NonNegative>) -> Self {
Self::new("sprout", amount)
}
/// Creates a [`ValuePoolBalance`] for the Sapling pool.
pub fn sapling(amount: Amount<NonNegative>) -> Self {
Self::new("sapling", amount)
}
/// Creates a [`ValuePoolBalance`] for the Orchard pool.
pub fn orchard(amount: Amount<NonNegative>) -> Self {
Self::new("orchard", amount)
}
/// Creates a [`ValuePoolBalance`] for the Deferred pool.
pub fn deferred(amount: Amount<NonNegative>) -> Self {
Self::new("deferred", amount)
}
/// Converts a [`ValueBalance`] to a list of [`ValuePoolBalance`]s.
pub fn from_value_balance(value_balance: ValueBalance<NonNegative>) -> [Self; 5] {
[
Self::transparent(value_balance.transparent_amount()),
Self::sprout(value_balance.sprout_amount()),
Self::sapling(value_balance.sapling_amount()),
Self::orchard(value_balance.orchard_amount()),
Self::deferred(value_balance.deferred_amount()),
]
}
}

View File

@ -815,6 +815,10 @@ pub enum ReadRequest {
/// with the current best chain tip.
Tip,
/// Returns [`ReadResponse::TipPoolValues(Option<(Height, block::Hash, ValueBalance)>)`](ReadResponse::TipPoolValues)
/// with the current best chain tip.
TipPoolValues,
/// Computes the depth in the current best chain of the block identified by the given hash.
///
/// Returns
@ -1065,6 +1069,7 @@ impl ReadRequest {
fn variant_name(&self) -> &'static str {
match self {
ReadRequest::Tip => "tip",
ReadRequest::TipPoolValues => "tip_pool_values",
ReadRequest::Depth(_) => "depth",
ReadRequest::Block(_) => "block",
ReadRequest::BlockHeader(_) => "block_header",

View File

@ -10,6 +10,7 @@ use zebra_chain::{
subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex},
transaction::{self, Transaction},
transparent,
value_balance::ValueBalance,
};
#[cfg(feature = "getblocktemplate-rpcs")]
@ -128,6 +129,17 @@ pub enum ReadResponse {
/// Response to [`ReadRequest::Tip`] with the current best chain tip.
Tip(Option<(block::Height, block::Hash)>),
/// Response to [`ReadRequest::TipPoolValues`] with
/// the current best chain tip and its [`ValueBalance`].
TipPoolValues {
/// The current best chain tip height.
tip_height: block::Height,
/// The current best chain tip hash.
tip_hash: block::Hash,
/// The value pool balance at the current best chain tip.
value_balance: ValueBalance<NonNegative>,
},
/// Response to [`ReadRequest::Depth`] with the depth of the specified block.
Depth(Option<u32>),
@ -287,7 +299,8 @@ impl TryFrom<ReadResponse> for Response {
ReadResponse::ValidBestChainTipNullifiersAndAnchors => Ok(Response::ValidBestChainTipNullifiersAndAnchors),
ReadResponse::TransactionIdsForBlock(_)
ReadResponse::TipPoolValues { .. }
| ReadResponse::TransactionIdsForBlock(_)
| ReadResponse::SaplingTree(_)
| ReadResponse::OrchardTree(_)
| ReadResponse::SaplingSubtrees(_)

View File

@ -1192,6 +1192,38 @@ impl Service<ReadRequest> for ReadStateService {
.wait_for_panics()
}
// Used by `getblockchaininfo` RPC method.
ReadRequest::TipPoolValues => {
let state = self.clone();
tokio::task::spawn_blocking(move || {
span.in_scope(move || {
let tip_with_value_balance = state
.non_finalized_state_receiver
.with_watch_data(|non_finalized_state| {
read::tip_with_value_balance(
non_finalized_state.best_chain(),
&state.db,
)
});
// The work is done in the future.
// TODO: Do this in the Drop impl with the variant name?
timer.finish(module_path!(), line!(), "ReadRequest::TipPoolValues");
let (tip_height, tip_hash, value_balance) = tip_with_value_balance?
.ok_or(BoxError::from("no chain tip available yet"))?;
Ok(ReadResponse::TipPoolValues {
tip_height,
tip_hash,
value_balance,
})
})
})
.wait_for_panics()
}
// Used by the StateService.
ReadRequest::Depth(hash) => {
let state = self.clone();

View File

@ -482,6 +482,17 @@ impl Chain {
)
}
/// Returns the non-finalized tip block height, hash, and total pool value balances.
pub fn non_finalized_tip_with_value_balance(
&self,
) -> (Height, block::Hash, ValueBalance<NonNegative>) {
(
self.non_finalized_tip_height(),
self.non_finalized_tip_hash(),
self.chain_value_pools,
)
}
/// Returns the Sprout note commitment tree of the tip of this [`Chain`],
/// including all finalized notes, and the non-finalized notes in this chain.
///

View File

@ -36,7 +36,7 @@ pub use block::{
pub use find::{
best_tip, block_locator, depth, finalized_state_contains_block_hash, find_chain_hashes,
find_chain_headers, hash_by_height, height_by_hash, next_median_time_past,
non_finalized_state_contains_block_hash, tip, tip_height,
non_finalized_state_contains_block_hash, tip, tip_height, tip_with_value_balance,
};
pub use tree::{orchard_subtrees, orchard_tree, sapling_subtrees, sapling_tree};

View File

@ -19,8 +19,10 @@ use std::{
use chrono::{DateTime, Utc};
use zebra_chain::{
amount::NonNegative,
block::{self, Block, Height},
serialization::DateTime32,
value_balance::ValueBalance,
};
use crate::{
@ -82,6 +84,40 @@ where
tip(chain, db).map(|(_height, hash)| hash)
}
/// Returns the tip of `chain` with its [`ValueBalance`].
/// If there is no chain, returns the tip of `db`.
pub fn tip_with_value_balance<C>(
chain: Option<C>,
db: &ZebraDb,
) -> Result<Option<(Height, block::Hash, ValueBalance<NonNegative>)>, BoxError>
where
C: AsRef<Chain>,
{
match chain.map(|chain| chain.as_ref().non_finalized_tip_with_value_balance()) {
tip_with_value_balance @ Some(_) => Ok(tip_with_value_balance),
None => {
// Retry the finalized state query if it was interrupted by a finalizing block.
//
// TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn
for _ in 0..=FINALIZED_STATE_QUERY_RETRIES {
let tip @ Some((tip_height, tip_hash)) = db.tip() else {
return Ok(None);
};
let value_balance = db.finalized_value_pool();
if tip == db.tip() {
return Ok(Some((tip_height, tip_hash, value_balance)));
}
}
Err("Zebra is committing too many blocks to the state, \
wait until it syncs to the chain tip"
.into())
}
}
}
/// Return the depth of block `hash` from the chain tip.
/// Searches `chain` for `hash`, then searches `db`.
pub fn depth<C>(chain: Option<C>, db: &ZebraDb, hash: block::Hash) -> Option<u32>