diff --git a/applications/tari_app_grpc/proto/base_node.proto b/applications/tari_app_grpc/proto/base_node.proto index e55ed9db02..e3309ad1f8 100644 --- a/applications/tari_app_grpc/proto/base_node.proto +++ b/applications/tari_app_grpc/proto/base_node.proto @@ -173,6 +173,8 @@ message NetworkDifficultyResponse { uint64 height = 3; uint64 timestamp = 4; uint64 pow_algo = 5; + uint64 sha3_estimated_hash_rate = 6; + uint64 monero_estimated_hash_rate = 7; } // A generic single value response for a specific height diff --git a/applications/tari_base_node/src/grpc/base_node_grpc_server.rs b/applications/tari_base_node/src/grpc/base_node_grpc_server.rs index d99c2e6183..6370beee86 100644 --- a/applications/tari_base_node/src/grpc/base_node_grpc_server.rs +++ b/applications/tari_base_node/src/grpc/base_node_grpc_server.rs @@ -59,6 +59,7 @@ use crate::{ builder::BaseNodeContext, grpc::{ blocks::{block_fees, block_heights, block_size, GET_BLOCKS_MAX_HEIGHTS, GET_BLOCKS_PAGE_SIZE}, + hash_rate::HashRateMovingAverage, helpers::{mean, median}, }, }; @@ -153,6 +154,11 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { } let (mut tx, rx) = mpsc::channel(cmp::min(num_requested as usize, GET_DIFFICULTY_PAGE_SIZE)); + let mut sha3_hash_rate_moving_average = + HashRateMovingAverage::new(PowAlgorithm::Sha3, self.consensus_rules.clone()); + let mut monero_hash_rate_moving_average = + HashRateMovingAverage::new(PowAlgorithm::Monero, self.consensus_rules.clone()); + task::spawn(async move { let page_iter = NonOverlappingIntegerPairIter::new(start_height, end_height + 1, GET_DIFFICULTY_PAGE_SIZE); for (start, end) in page_iter { @@ -176,33 +182,31 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { return; } - let mut headers_iter = headers.iter().peekable(); - - while let Some(chain_header) = headers_iter.next() { - let current_difficulty = chain_header.accumulated_data().target_difficulty.as_u64(); - let current_timestamp = chain_header.header().timestamp.as_u64(); + for chain_header in headers.iter() { + let current_difficulty = chain_header.accumulated_data().target_difficulty; + let current_timestamp = chain_header.header().timestamp; let current_height = chain_header.header().height; - let pow_algo = chain_header.header().pow.pow_algo.as_u64(); - - let estimated_hash_rate = headers_iter - .peek() - .map(|chain_header| chain_header.header().timestamp.as_u64()) - .and_then(|peeked_timestamp| { - // Sometimes blocks can have the same timestamp, lucky miner and some - // clock drift. - peeked_timestamp - .checked_sub(current_timestamp) - .filter(|td| *td > 0) - .map(|time_diff| current_timestamp / time_diff) - }) - .unwrap_or(0); + let pow_algo = chain_header.header().pow.pow_algo; + + // update the moving average calculation with the header data + let current_hash_rate_moving_average = match pow_algo { + PowAlgorithm::Monero => &mut monero_hash_rate_moving_average, + PowAlgorithm::Sha3 => &mut sha3_hash_rate_moving_average, + }; + current_hash_rate_moving_average.add(current_height, current_difficulty); + + let sha3_estimated_hash_rate = sha3_hash_rate_moving_average.average(); + let monero_estimated_hash_rate = monero_hash_rate_moving_average.average(); + let estimated_hash_rate = sha3_estimated_hash_rate + monero_estimated_hash_rate; let difficulty = tari_rpc::NetworkDifficultyResponse { - difficulty: current_difficulty, + difficulty: current_difficulty.as_u64(), estimated_hash_rate, + sha3_estimated_hash_rate, + monero_estimated_hash_rate, height: current_height, - timestamp: current_timestamp, - pow_algo, + timestamp: current_timestamp.as_u64(), + pow_algo: pow_algo.as_u64(), }; if let Err(err) = tx.send(Ok(difficulty)).await { diff --git a/applications/tari_base_node/src/grpc/hash_rate.rs b/applications/tari_base_node/src/grpc/hash_rate.rs new file mode 100644 index 0000000000..c7173b622b --- /dev/null +++ b/applications/tari_base_node/src/grpc/hash_rate.rs @@ -0,0 +1,198 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::collections::VecDeque; + +use tari_core::{ + consensus::ConsensusManager, + proof_of_work::{Difficulty, PowAlgorithm}, +}; + +/// The number of past blocks to be used on moving averages for (smooth) estimated hashrate +/// We consider a 60 minute time window reasonable, that means 12 SHA3 blocks and 18 Monero blocks +const SHA3_HASH_RATE_MOVING_AVERAGE_WINDOW: usize = 12; +const MONERO_HASH_RATE_MOVING_AVERAGE_WINDOW: usize = 18; + +/// Calculates a linear weighted moving average for hash rate calculations +pub struct HashRateMovingAverage { + pow_algo: PowAlgorithm, + consensus_manager: ConsensusManager, + window_size: usize, + hash_rates: VecDeque, + average: u64, +} + +impl HashRateMovingAverage { + pub fn new(pow_algo: PowAlgorithm, consensus_manager: ConsensusManager) -> Self { + let window_size = match pow_algo { + PowAlgorithm::Monero => MONERO_HASH_RATE_MOVING_AVERAGE_WINDOW, + PowAlgorithm::Sha3 => SHA3_HASH_RATE_MOVING_AVERAGE_WINDOW, + }; + let hash_rates = VecDeque::with_capacity(window_size); + + Self { + pow_algo, + consensus_manager, + window_size, + hash_rates, + average: 0, + } + } + + /// Adds a new hash rate entry in the moving average and recalculates the average + pub fn add(&mut self, height: u64, difficulty: Difficulty) { + // target block time for the current block is provided by the consensus rules + let target_time = self + .consensus_manager + .consensus_constants(height) + .get_diff_target_block_interval(self.pow_algo); + + // remove old entries if we are at max block window + if self.is_full() { + self.hash_rates.pop_back(); + } + + // add the new hash rate to the list + let current_hash_rate = difficulty.as_u64() / target_time; + self.hash_rates.push_front(current_hash_rate); + + // after adding the hash rate we need to recalculate the average + self.average = self.calculate_average(); + } + + fn is_full(&self) -> bool { + self.hash_rates.len() >= self.window_size + } + + fn calculate_average(&self) -> u64 { + // this check is not strictly necessary as this is only called after adding an item + // but let's be on the safe side for future changes + if self.hash_rates.is_empty() { + return 0; + } + + let sum: u64 = self.hash_rates.iter().sum(); + let count = self.hash_rates.len() as u64; + sum / count + } + + pub fn average(&self) -> u64 { + self.average + } +} + +#[cfg(test)] +mod test { + use tari_core::{ + consensus::{ConsensusConstants, ConsensusManagerBuilder}, + proof_of_work::{Difficulty, PowAlgorithm}, + }; + use tari_p2p::Network; + + use super::HashRateMovingAverage; + + #[test] + fn window_is_empty() { + let hash_rate_ma = create_hash_rate_ma(PowAlgorithm::Sha3); + assert!(!hash_rate_ma.is_full()); + assert_eq!(hash_rate_ma.calculate_average(), 0); + assert_eq!(hash_rate_ma.average(), 0); + } + + #[test] + fn window_is_full() { + let mut hash_rate_ma = create_hash_rate_ma(PowAlgorithm::Sha3); + let window_size = hash_rate_ma.window_size; + + // we check that the window is not full when we insert less items than the window size + for _ in 0..window_size - 1 { + hash_rate_ma.add(0, Difficulty::from(0)); + assert!(!hash_rate_ma.is_full()); + } + + // from this point onwards, the window should be always full + for _ in 0..10 { + hash_rate_ma.add(0, Difficulty::from(0)); + assert!(hash_rate_ma.is_full()); + } + } + + // Checks that the moving average hash rate at every block is correct + // We use larger sample data than the SHA window size (12 periods) to check bounds + // We assumed a constant target block time of 300 secs (the SHA3 target time for Dibbler) + // These expected hash rate values where calculated in a spreadsheet + #[test] + fn correct_moving_average_calculation() { + let mut hash_rate_ma = create_hash_rate_ma(PowAlgorithm::Sha3); + + assert_hash_rate(&mut hash_rate_ma, 0, 100_000, 333); + assert_hash_rate(&mut hash_rate_ma, 1, 120_100, 366); + assert_hash_rate(&mut hash_rate_ma, 2, 110_090, 366); + assert_hash_rate(&mut hash_rate_ma, 3, 121_090, 375); + assert_hash_rate(&mut hash_rate_ma, 4, 150_000, 400); + assert_hash_rate(&mut hash_rate_ma, 5, 155_000, 419); + assert_hash_rate(&mut hash_rate_ma, 6, 159_999, 435); + assert_hash_rate(&mut hash_rate_ma, 7, 160_010, 448); + assert_hash_rate(&mut hash_rate_ma, 8, 159_990, 457); + assert_hash_rate(&mut hash_rate_ma, 9, 140_000, 458); + assert_hash_rate(&mut hash_rate_ma, 10, 137_230, 458); + assert_hash_rate(&mut hash_rate_ma, 11, 130_000, 456); + assert_hash_rate(&mut hash_rate_ma, 12, 120_000, 461); + assert_hash_rate(&mut hash_rate_ma, 13, 140_000, 467); + } + + // Our moving average windows are very small (12 and 15 depending on PoW algorithm) + // So we will never get an overflow when we do the sums for the average calculation (we divide by target time) + // Anyways, just in case we go with huge windows in the future, this test should fail with a panic due to overflow + #[test] + fn should_not_overflow() { + let mut sha3_hash_rate_ma = create_hash_rate_ma(PowAlgorithm::Sha3); + let mut monero_hash_rate_ma = create_hash_rate_ma(PowAlgorithm::Monero); + try_to_overflow(&mut sha3_hash_rate_ma); + try_to_overflow(&mut monero_hash_rate_ma); + } + + fn try_to_overflow(hash_rate_ma: &mut HashRateMovingAverage) { + let window_size = hash_rate_ma.window_size; + + for _ in 0..window_size { + hash_rate_ma.add(0, Difficulty::from(u64::MAX)); + } + } + + fn create_hash_rate_ma(pow_algo: PowAlgorithm) -> HashRateMovingAverage { + let consensus_manager = ConsensusManagerBuilder::new(Network::Dibbler) + .add_consensus_constants(ConsensusConstants::dibbler()[0].clone()) + .build(); + HashRateMovingAverage::new(pow_algo, consensus_manager) + } + + fn assert_hash_rate( + moving_average: &mut HashRateMovingAverage, + height: u64, + difficulty: u64, + expected_hash_rate: u64, + ) { + moving_average.add(height, Difficulty::from(difficulty)); + assert_eq!(moving_average.average(), expected_hash_rate); + } +} diff --git a/applications/tari_base_node/src/grpc/mod.rs b/applications/tari_base_node/src/grpc/mod.rs index a7070dbb54..1c8825511a 100644 --- a/applications/tari_base_node/src/grpc/mod.rs +++ b/applications/tari_base_node/src/grpc/mod.rs @@ -22,4 +22,5 @@ pub mod base_node_grpc_server; pub mod blocks; +pub mod hash_rate; pub mod helpers; diff --git a/applications/tari_explorer/routes/index.js b/applications/tari_explorer/routes/index.js index 23cb6cb1b7..4088af8207 100644 --- a/applications/tari_explorer/routes/index.js +++ b/applications/tari_explorer/routes/index.js @@ -79,6 +79,18 @@ router.get("/", async function (req, res) { // -- mempool let mempool = await client.getMempoolTransactions({}); + // estimated hash rates + let lastDifficulties = await client.getNetworkDifficulty({ from_tip: 100 }); + let totalHashRates = getHashRates(lastDifficulties, "estimated_hash_rate"); + let moneroHashRates = getHashRates( + lastDifficulties, + "monero_estimated_hash_rate" + ); + let shaHashRates = getHashRates( + lastDifficulties, + "sha3_estimated_hash_rate" + ); + // console.log(mempool); for (let i = 0; i < mempool.length; i++) { let sum = 0; @@ -103,6 +115,11 @@ router.get("/", async function (req, res) { blockTimes: getBlockTimes(last100Headers), moneroTimes: getBlockTimes(last100Headers, "0"), shaTimes: getBlockTimes(last100Headers, "1"), + currentHashRate: totalHashRates[totalHashRates.length - 1], + currentShaHashRate: shaHashRates[shaHashRates.length - 1], + shaHashRates, + currentMoneroHashRate: moneroHashRates[moneroHashRates.length - 1], + moneroHashRates, }; res.render("index", result); } catch (error) { @@ -111,6 +128,15 @@ router.get("/", async function (req, res) { } }); +function getHashRates(difficulties, property) { + const end_idx = difficulties.length - 1; + const start_idx = end_idx - 60; + + return difficulties + .map((d) => parseInt(d[property])) + .slice(start_idx, end_idx); +} + function getBlockTimes(last100Headers, algo) { let blocktimes = []; let i = 0; diff --git a/applications/tari_explorer/views/index.hbs b/applications/tari_explorer/views/index.hbs index f874c594fe..f7a60b3a04 100644 --- a/applications/tari_explorer/views/index.hbs +++ b/applications/tari_explorer/views/index.hbs @@ -90,6 +90,38 @@
+ + + + + + + + + +
+

Estimated Hash Rate

+ Current total estimated Hash Rate: + {{this.currentHashRate}} + H/s +
+
+

Monero

+ Current estimated Hash Rate: + {{this.currentMoneroHashRate}} + H/s +
{{chart this.moneroHashRates 15}}
+      
+
+

SHA3

+ Current estimated Hash Rate: + {{this.currentShaHashRate}} + H/s +
{{chart this.shaHashRates 15}}
+      
+
+
+

{{title}}

diff --git a/clients/base_node_grpc_client/src/index.js b/clients/base_node_grpc_client/src/index.js index 3f6e151962..8a83925d9f 100644 --- a/clients/base_node_grpc_client/src/index.js +++ b/clients/base_node_grpc_client/src/index.js @@ -41,7 +41,8 @@ function Client(address = "127.0.0.1:18142") { "getMempoolTransactions", "getTipInfo", "searchUtxos", - "getTokens" + "getTokens", + "getNetworkDifficulty" ]; methods.forEach((method) => { this[method] = (arg) => this.inner[method]().sendMessage(arg); diff --git a/integration_tests/features/support/steps.js b/integration_tests/features/support/steps.js index 665a98172e..c3e919e520 100644 --- a/integration_tests/features/support/steps.js +++ b/integration_tests/features/support/steps.js @@ -489,12 +489,14 @@ When(/I request the difficulties of a node (.*)/, async function (node) { }); Then("difficulties are available", function () { - assert(this.lastResult.length, 3); + assert.strictEqual(this.lastResult.length, 3); // check genesis block, chain in reverse height order - assert(this.lastResult[2].difficulty, "1"); - assert(this.lastResult[2].estimated_hash_rate, "0"); - assert(this.lastResult[2].height, "1"); - assert(this.lastResult[2].pow_algo, "0"); + expect(this.lastResult[2].difficulty).to.equal("1"); + expect(this.lastResult[2].estimated_hash_rate).to.equal("0"); + expect(this.lastResult[2].sha3_estimated_hash_rate).to.equal("0"); + expect(this.lastResult[2].monero_estimated_hash_rate).to.equal("0"); + expect(this.lastResult[2].height).to.equal("2"); + expect(this.lastResult[2].pow_algo).to.equal("0"); }); When(