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

fix: estimated hashrate calculation is incorrect #3996

Merged
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions applications/tari_app_grpc/proto/base_node.proto
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a comment here for why these values are different

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

.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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading