Skip to content

Commit

Permalink
fix: estimated hashrate calculation is incorrect (#3996)
Browse files Browse the repository at this point in the history
Description
---
* Created new fields in the base_node grpc api for sha3 and monero hash rates, using an independent moving average for each one
* Kept the `estimated_hash_rate` field but now it corresponds to the sum of the sha3 and monero hash rates, so existing clients (i.e. web explorer) will not break
* Modified the text explorer to show the estimated hash rates

Motivation and Context
---
The current hash rate calculation is incorrect, we want to use a separated moving average for each PoW algo independently

How Has This Been Tested?
---
Unit testing for the moving average hash rate calculation
  • Loading branch information
mrnaveira committed Apr 4, 2022
1 parent e590547 commit 4587fc0
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 28 deletions.
2 changes: 2 additions & 0 deletions applications/tari_app_grpc/proto/base_node.proto
Expand Up @@ -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
Expand Down
48 changes: 26 additions & 22 deletions applications/tari_base_node/src/grpc/base_node_grpc_server.rs
Expand Up @@ -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},
},
};
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
198 changes: 198 additions & 0 deletions 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<u64>,
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);
}
}
1 change: 1 addition & 0 deletions applications/tari_base_node/src/grpc/mod.rs
Expand Up @@ -22,4 +22,5 @@

pub mod base_node_grpc_server;
pub mod blocks;
pub mod hash_rate;
pub mod helpers;
26 changes: 26 additions & 0 deletions applications/tari_explorer/routes/index.js
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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;
Expand Down
32 changes: 32 additions & 0 deletions applications/tari_explorer/views/index.hbs
Expand Up @@ -90,6 +90,38 @@
</table>
<br />

<table class="noborder">
<tr>
<td>
<h3>Estimated Hash Rate</h3>
Current total estimated Hash Rate:
{{this.currentHashRate}}
H/s
</td>
<td>
</td>
</tr>
<tr>
<td>
<h3>Monero</h3>
Current estimated Hash Rate:
{{this.currentMoneroHashRate}}
H/s
<pre>{{chart this.moneroHashRates 15}}
</pre>
</td>
<td>
<h3>SHA3</h3>
Current estimated Hash Rate:
{{this.currentShaHashRate}}
H/s
<pre>{{chart this.shaHashRates 15}}
</pre>
</td>
</tr>
</table>
<br />

<h2>{{title}}</h2>
<table>
<thead>
Expand Down
3 changes: 2 additions & 1 deletion clients/base_node_grpc_client/src/index.js
Expand Up @@ -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);
Expand Down

0 comments on commit 4587fc0

Please sign in to comment.