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

merge queue: embarking main (ece9a8e) and #9215 together #9247

Closed
wants to merge 11 commits into from
276 changes: 201 additions & 75 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ use zebra_chain::{
subtree::NoteCommitmentSubtreeIndex,
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
transparent::{self, Address},
value_balance::ValueBalance,
work::{
difficulty::{CompactDifficulty, ExpandedDifficulty},
difficulty::{CompactDifficulty, ExpandedDifficulty, ParameterDifficulty, U256},
equihash::Solution,
},
};
use zebra_consensus::ParameterCheckpoint;
use zebra_node_services::mempool;
use zebra_state::{HashOrHeight, OutputIndex, OutputLocation, TransactionLocation};
use zebra_state::{
HashOrHeight, OutputIndex, OutputLocation, ReadRequest, ReadResponse, TransactionLocation,
};

use crate::{
methods::trees::{GetSubtrees, GetTreestate, SubtreeRpcData},
Expand Down Expand Up @@ -546,75 +548,65 @@ where

#[allow(clippy::unwrap_in_result)]
async fn get_blockchain_info(&self) -> Result<GetBlockChainInfo> {
let network = self.network.clone();
let debug_force_finished_sync = self.debug_force_finished_sync;
let mut state = self.state.clone();
let network = &self.network;

let (usage_info_rsp, tip_pool_values_rsp, chain_tip_difficulty) = {
use zebra_state::ReadRequest::*;
let state_call = |request| self.state.clone().oneshot(request);
tokio::join!(
state_call(UsageInfo),
state_call(TipPoolValues),
chain_tip_difficulty(network.clone(), self.state.clone())
)
};

// `chain` field
let chain = network.bip70_network_name();
let (size_on_disk, (tip_height, tip_hash), value_balance, difficulty) = {
use zebra_state::ReadResponse::*;

let (tip_height, tip_hash, tip_block_time, value_balance) = match state
.ready()
.and_then(|service| service.call(zebra_state::ReadRequest::TipPoolValues))
.await
{
Ok(zebra_state::ReadResponse::TipPoolValues {
tip_height,
tip_hash,
value_balance,
}) => {
let request = zebra_state::ReadRequest::BlockHeader(tip_hash.into());
let response: zebra_state::ReadResponse = state
.ready()
.and_then(|service| service.call(request))
.await
.map_misc_error()?;

if let zebra_state::ReadResponse::BlockHeader { header, .. } = response {
(tip_height, tip_hash, header.time, value_balance)
} else {
unreachable!("unmatched response to a TipPoolValues request")
}
}
_ => {
let request =
zebra_state::ReadRequest::BlockHeader(HashOrHeight::Height(Height::MIN));
let response: zebra_state::ReadResponse = state
.ready()
.and_then(|service| service.call(request))
.await
.map_misc_error()?;

if let zebra_state::ReadResponse::BlockHeader {
header,
hash,
height,
..
} = response
{
(height, hash, header.time, ValueBalance::zero())
} else {
unreachable!("unmatched response to a BlockHeader request")
}
}
let UsageInfo(size_on_disk) = usage_info_rsp.map_misc_error()? else {
unreachable!("unmatched response to a TipPoolValues request")
};

let (tip, value_balance) = match tip_pool_values_rsp {
Ok(TipPoolValues {
tip_height,
tip_hash,
value_balance,
}) => ((tip_height, tip_hash), value_balance),
Ok(_) => unreachable!("unmatched response to a TipPoolValues request"),
Err(_) => ((Height::MIN, network.genesis_hash()), Default::default()),
};

let difficulty = chain_tip_difficulty.unwrap_or_else(|_| {
(U256::from(network.target_difficulty_limit()) >> 128).as_u128() as f64
});
(size_on_disk, tip, value_balance, difficulty)
};

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
};
let (estimated_height, verification_progress) = self
.latest_chain_tip
.best_tip_height_and_block_time()
.map(|(tip_height, tip_block_time)| {
let 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 height =
if tip_block_time > now || height < tip_height || debug_force_finished_sync {
tip_height
} else {
height
};

(height, f64::from(tip_height.0) / f64::from(height.0))
})
// TODO: Add a `genesis_block_time()` method on `Network` to use here.
.unwrap_or((Height::MIN, 0.0));

// `upgrades` object
//
Expand Down Expand Up @@ -647,29 +639,40 @@ where
(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)
NetworkUpgrade::current(network, tip_height)
.branch_id()
.unwrap_or(ConsensusBranchId::RPC_MISSING_ID),
),
next_block: ConsensusBranchIdHex(
NetworkUpgrade::current(&network, next_block_height)
NetworkUpgrade::current(network, next_block_height)
.branch_id()
.unwrap_or(ConsensusBranchId::RPC_MISSING_ID),
),
};

let response = GetBlockChainInfo {
chain,
chain: network.bip70_network_name(),
blocks: tip_height,
best_block_hash: tip_hash,
estimated_height,
value_pools: types::ValuePoolBalance::from_value_balance(value_balance),
chain_supply: types::Balance::chain_supply(value_balance),
value_pools: types::Balance::value_pools(value_balance),
upgrades,
consensus,
headers: tip_height,
difficulty,
verification_progress,
// TODO: store work in the finalized state for each height (#7109)
chain_work: 0,
pruned: false,
size_on_disk,
// TODO: Investigate whether this needs to be implemented (it's sprout-only in zcashd)
commitments: 0,
};

Ok(response)
}

async fn get_address_balance(&self, address_strings: AddressStrings) -> Result<AddressBalance> {
let state = self.state.clone();

Expand Down Expand Up @@ -1540,14 +1543,38 @@ impl GetInfo {
/// Response to a `getblockchaininfo` RPC request.
///
/// See the notes for the [`Rpc::get_blockchain_info` method].
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct GetBlockChainInfo {
/// Current network name as defined in BIP70 (main, test, regtest)
chain: String,

/// The current number of blocks processed in the server, numeric
blocks: Height,

/// The current number of headers we have validated in the best chain, that is,
/// the height of the best chain.
headers: Height,

/// The estimated network solution rate in Sol/s.
difficulty: f64,

/// The verification progress relative to the estimated network chain tip.
#[serde(rename = "verificationprogress")]
verification_progress: f64,

/// The total amount of work in the best chain, hex-encoded.
#[serde(rename = "chainwork")]
chain_work: u64,

/// Whether this node is pruned, currently always false in Zebra.
pruned: bool,

/// The estimated size of the block and undo files on disk
size_on_disk: u64,

/// The current number of note commitments in the commitment tree
commitments: u64,

/// The hash of the currently best block, in big-endian order, hex-encoded
#[serde(rename = "bestblockhash", with = "hex")]
best_block_hash: block::Hash,
Expand All @@ -1558,9 +1585,13 @@ pub struct GetBlockChainInfo {
#[serde(rename = "estimatedheight")]
estimated_height: Height,

/// Chain supply balance
#[serde(rename = "chainSupply")]
chain_supply: types::Balance,

/// Value pool balances
#[serde(rename = "valuePools")]
value_pools: [types::ValuePoolBalance; 5],
value_pools: [types::Balance; 5],

/// Status of network upgrades
upgrades: IndexMap<ConsensusBranchIdHex, NetworkUpgradeInfo>,
Expand All @@ -1576,35 +1607,60 @@ impl Default for GetBlockChainInfo {
blocks: Height(1),
best_block_hash: block::Hash([0; 32]),
estimated_height: Height(1),
value_pools: types::ValuePoolBalance::zero_pools(),
chain_supply: types::Balance::chain_supply(Default::default()),
value_pools: types::Balance::zero_pools(),
upgrades: IndexMap::new(),
consensus: TipConsensusBranch {
chain_tip: ConsensusBranchIdHex(ConsensusBranchId::default()),
next_block: ConsensusBranchIdHex(ConsensusBranchId::default()),
},
headers: Height(1),
difficulty: 0.0,
verification_progress: 0.0,
chain_work: 0,
pruned: false,
size_on_disk: 0,
commitments: 0,
}
}
}

impl GetBlockChainInfo {
/// Creates a new [`GetBlockChainInfo`] instance.
#[allow(clippy::too_many_arguments)]
pub fn new(
chain: String,
blocks: Height,
best_block_hash: block::Hash,
estimated_height: Height,
value_pools: [types::ValuePoolBalance; 5],
chain_supply: types::Balance,
value_pools: [types::Balance; 5],
upgrades: IndexMap<ConsensusBranchIdHex, NetworkUpgradeInfo>,
consensus: TipConsensusBranch,
headers: Height,
difficulty: f64,
verification_progress: f64,
chain_work: u64,
pruned: bool,
size_on_disk: u64,
commitments: u64,
) -> Self {
Self {
chain,
blocks,
best_block_hash,
estimated_height,
chain_supply,
value_pools,
upgrades,
consensus,
headers,
difficulty,
verification_progress,
chain_work,
pruned,
size_on_disk,
commitments,
}
}

Expand Down Expand Up @@ -1633,7 +1689,7 @@ impl GetBlockChainInfo {
}

/// Returns the value pool balances.
pub fn value_pools(&self) -> &[types::ValuePoolBalance; 5] {
pub fn value_pools(&self) -> &[types::Balance; 5] {
&self.value_pools
}

Expand Down Expand Up @@ -2456,3 +2512,73 @@ mod opthex {
}
}
}
/// Returns the proof-of-work difficulty as a multiple of the minimum difficulty.
pub(crate) async fn chain_tip_difficulty<State>(network: Network, mut state: State) -> Result<f64>
where
State: Service<
zebra_state::ReadRequest,
Response = zebra_state::ReadResponse,
Error = zebra_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
State::Future: Send,
{
let request = ReadRequest::ChainInfo;

// # TODO
// - add a separate request like BestChainNextMedianTimePast, but skipping the
// consistency check, because any block's difficulty is ok for display
// - return 1.0 for a "not enough blocks in the state" error, like `zcashd`:
// <https://github.com/zcash/zcash/blob/7b28054e8b46eb46a9589d0bdc8e29f9fa1dc82d/src/rpc/blockchain.cpp#L40-L41>
let response = state
.ready()
.and_then(|service| service.call(request))
.await
.map_err(|error| ErrorObject::owned(0, error.to_string(), None::<()>))?;

let chain_info = match response {
ReadResponse::ChainInfo(info) => info,
_ => unreachable!("unmatched response to a chain info request"),
};

// This RPC is typically used for display purposes, so it is not consensus-critical.
// But it uses the difficulty consensus rules for its calculations.
//
// Consensus:
// https://zips.z.cash/protocol/protocol.pdf#nbits
//
// The zcashd implementation performs to_expanded() on f64,
// and then does an inverse division:
// https://github.com/zcash/zcash/blob/d6e2fada844373a8554ee085418e68de4b593a6c/src/rpc/blockchain.cpp#L46-L73
//
// But in Zebra we divide the high 128 bits of each expanded difficulty. This gives
// a similar result, because the lower 128 bits are insignificant after conversion
// to `f64` with a 53-bit mantissa.
//
// `pow_limit >> 128 / difficulty >> 128` is the same as the work calculation
// `(2^256 / pow_limit) / (2^256 / difficulty)`, but it's a bit more accurate.
//
// To simplify the calculation, we don't scale for leading zeroes. (Bitcoin's
// difficulty currently uses 68 bits, so even it would still have full precision
// using this calculation.)

// Get expanded difficulties (256 bits), these are the inverse of the work
let pow_limit: U256 = network.target_difficulty_limit().into();
let Some(difficulty) = chain_info.expected_difficulty.to_expanded() else {
return Ok(0.0);
};

// Shift out the lower 128 bits (256 bits, but the top 128 are all zeroes)
let pow_limit = pow_limit >> 128;
let difficulty = U256::from(difficulty) >> 128;

// Convert to u128 then f64.
// We could also convert U256 to String, then parse as f64, but that's slower.
let pow_limit = pow_limit.as_u128() as f64;
let difficulty = difficulty.as_u128() as f64;

// Invert the division to give approximately: `work(difficulty) / work(pow_limit)`
Ok(pow_limit / difficulty)
}
Loading
Loading