diff --git a/zebra-chain/src/chain_tip.rs b/zebra-chain/src/chain_tip.rs index c19c556e7..04e98ecbf 100644 --- a/zebra-chain/src/chain_tip.rs +++ b/zebra-chain/src/chain_tip.rs @@ -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. /// diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index c17a742a5..ae5deb7a5 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -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; + fn get_blockchain_info(&self) -> BoxFuture>; /// 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 { - let network = &self.network; + fn get_blockchain_info(&self) -> BoxFuture> { + 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, @@ -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()), diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index 464018979..c8c83e931 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -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> { 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, @@ -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(); } } } diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types.rs index a2f5ccc26..41046f7b2 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types.rs @@ -11,4 +11,3 @@ pub mod transaction; pub mod unified_address; pub mod validate_address; pub mod z_validate_address; -pub mod zec; diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/subsidy.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/subsidy.rs index d7e491de5..6d64dcbde 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types/subsidy.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/subsidy.rs @@ -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)] diff --git a/zebra-rpc/src/methods/tests/prop.rs b/zebra-rpc/src/methods/tests/prop.rs index 38c3c6b08..c2a9c70a3 100644 --- a/zebra-rpc/src/methods/tests/prop.rs +++ b/zebra-rpc/src/methods/tests/prop.rs @@ -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?; diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index 55c4fee86..f4d780408 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -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); diff --git a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap index 595a29bca..64663b861 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap @@ -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", diff --git a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap index 10a6f7f0b..d5c7d4b53 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap @@ -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", diff --git a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height@nu6testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height@nu6testnet_10.snap index d2250e7fe..393e53f5a 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height@nu6testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height@nu6testnet_10.snap @@ -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", diff --git a/zebra-rpc/src/methods/types.rs b/zebra-rpc/src/methods/types.rs new file mode 100644 index 000000000..d29e697f0 --- /dev/null +++ b/zebra-rpc/src/methods/types.rs @@ -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; diff --git a/zebra-rpc/src/methods/types/get_blockchain_info.rs b/zebra-rpc/src/methods/types/get_blockchain_info.rs new file mode 100644 index 000000000..a2d1e7816 --- /dev/null +++ b/zebra-rpc/src/methods/types/get_blockchain_info.rs @@ -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, + /// Total amount in the pool, in zatoshis + chain_value_zat: Amount, +} + +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) -> 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) -> Self { + Self::new("transparent", amount) + } + + /// Creates a [`ValuePoolBalance`] for the Sprout pool. + pub fn sprout(amount: Amount) -> Self { + Self::new("sprout", amount) + } + + /// Creates a [`ValuePoolBalance`] for the Sapling pool. + pub fn sapling(amount: Amount) -> Self { + Self::new("sapling", amount) + } + + /// Creates a [`ValuePoolBalance`] for the Orchard pool. + pub fn orchard(amount: Amount) -> Self { + Self::new("orchard", amount) + } + + /// Creates a [`ValuePoolBalance`] for the Deferred pool. + pub fn deferred(amount: Amount) -> Self { + Self::new("deferred", amount) + } + + /// Converts a [`ValueBalance`] to a list of [`ValuePoolBalance`]s. + pub fn from_value_balance(value_balance: ValueBalance) -> [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()), + ] + } +} diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/zec.rs b/zebra-rpc/src/methods/types/zec.rs similarity index 100% rename from zebra-rpc/src/methods/get_block_template_rpcs/types/zec.rs rename to zebra-rpc/src/methods/types/zec.rs diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 338c85301..28740a336 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -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", diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 242191f82..22e610838 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -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, + }, + /// Response to [`ReadRequest::Depth`] with the depth of the specified block. Depth(Option), @@ -287,7 +299,8 @@ impl TryFrom for Response { ReadResponse::ValidBestChainTipNullifiersAndAnchors => Ok(Response::ValidBestChainTipNullifiersAndAnchors), - ReadResponse::TransactionIdsForBlock(_) + ReadResponse::TipPoolValues { .. } + | ReadResponse::TransactionIdsForBlock(_) | ReadResponse::SaplingTree(_) | ReadResponse::OrchardTree(_) | ReadResponse::SaplingSubtrees(_) diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 0fbe8d8ea..2116ab104 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1192,6 +1192,38 @@ impl Service 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(); diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index e25b1fd17..12ee05287 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -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) { + ( + 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. /// diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index b8d022392..0188ca1bf 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -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}; diff --git a/zebra-state/src/service/read/find.rs b/zebra-state/src/service/read/find.rs index 187510c9b..e9d557dbf 100644 --- a/zebra-state/src/service/read/find.rs +++ b/zebra-state/src/service/read/find.rs @@ -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( + chain: Option, + db: &ZebraDb, +) -> Result)>, BoxError> +where + C: AsRef, +{ + 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(chain: Option, db: &ZebraDb, hash: block::Hash) -> Option