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

Apply LWMA algo with diff adjustiment trait #971

Merged
merged 1 commit into from
Nov 6, 2019
Merged
Show file tree
Hide file tree
Changes from all 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: 1 addition & 1 deletion applications/tari_testnet_miner/src/miner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ impl Miner {
return Err(MinerError::MissingBlock);
}
let interval = self.block.as_ref().unwrap().header.timestamp.timestamp() - old_header.timestamp.timestamp();
let difficulty = Difficulty::calculate_req_difficulty(interval, self.difficulty);
let difficulty = Difficulty::min(); // replace with new function: Difficulty::calculate_req_difficulty(interval, self.difficulty);

let (tx, mut rx): (Sender<BlockHeader>, Receiver<BlockHeader>) = mpsc::channel(1);
for _ in 0..self.thread_count {
Expand Down
70 changes: 20 additions & 50 deletions base_layer/core/src/proof_of_work/difficulty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
// 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 crate::proof_of_work::error::DifficultyAdjustmentError;
use bitflags::_core::ops::Div;
use newtype_ops::newtype_ops;
use serde::{Deserialize, Serialize};
Expand All @@ -28,42 +29,28 @@ use std::fmt;
/// Minimum difficulty, enforced in diff retargetting
/// avoids getting stuck when trying to increase difficulty subject to dampening
pub const MIN_DIFFICULTY: u64 = 1;
/// This is the time in seconds that should be the ideal time between making new blocks
pub const BLOCK_INTERVAL: u64 = 60;
/// This is the amount of blocks between difficulty adjustments.
pub const BLOCKS_PER_ADJUSTMENT: u64 = 2016;

/// The difficulty is defined as the maximum target divided by the block hash.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Deserialize, Serialize)]
pub struct Difficulty(u64);

impl Difficulty {
/// Difficulty of MIN_DIFFICULTY
pub fn min() -> Difficulty {
pub const fn min() -> Difficulty {
Difficulty(MIN_DIFFICULTY)
}

/// This function will calculate the required difficulty given the time taken to calculate 2016 blocks
/// The interval is the difference in seconds between the two headers
pub fn calculate_req_difficulty(interval: i64, difficulty: Difficulty) -> Difficulty {
let target_time = (BLOCK_INTERVAL * BLOCKS_PER_ADJUSTMENT) as f32; // 60 seconds per block, 2016 blocks
let deviation = (interval as f32) - target_time;
let mut difficulty_multiplier = 1.0 - (deviation / target_time);
// cap the max adjustment to 50%
if difficulty_multiplier >= 1.5 {
difficulty_multiplier = 1.5;
};
if difficulty_multiplier <= 0.5 {
difficulty_multiplier = 0.5;
};
// return a new difficulty that is proportionally larger or smaller depending on the time diff.
Difficulty((difficulty.0 as f32 * (difficulty_multiplier)) as u64)
}

/// Return the difficulty as a u64
pub fn as_u64(&self) -> u64 {
self.0
}

pub fn checked_sub(&self, other: Difficulty) -> Option<Difficulty> {
match self.0.checked_sub(other.0) {
None => None,
Some(v) => Some(Difficulty(v)),
}
}
}

impl Default for Difficulty {
Expand Down Expand Up @@ -101,6 +88,17 @@ impl From<u64> for Difficulty {
}
}

/// General difficulty adjustment algorithm trait. The key method is `get_difficulty`, which returns the target
/// difficulty given a set of historical achieved difficulties; supplied through the `add` method.
pub trait DifficultyAdjustment {
/// Adds the latest block timestamp (in seconds) and total accumulated difficulty. If the new data point violates
/// some difficulty criteria, then `add` returns an error with the type of failure indicated
fn add(&mut self, timestamp: u64, accumulated_difficulty: Difficulty) -> Result<(), DifficultyAdjustmentError>;

/// Return the calculated target difficulty for the next block.
fn get_difficulty(&self) -> Difficulty;
}

#[cfg(test)]
mod test {
use crate::proof_of_work::difficulty::Difficulty;
Expand All @@ -114,32 +112,4 @@ mod test {
assert_eq!(Difficulty::default() + Difficulty::from(42), Difficulty::from(42));
assert_eq!(&Difficulty::from(15) + &Difficulty::from(5), Difficulty::from(20));
}

#[test]
fn calc_difficulty() {
assert_eq!(
Difficulty::min(),
Difficulty::calculate_req_difficulty(0, Difficulty::min()),
);
let diff = Difficulty::from(9_000);
let new_diff = Difficulty::calculate_req_difficulty(60 * 2016, diff);
assert_eq!(diff, new_diff);
let new_diff = Difficulty::calculate_req_difficulty(60 * 2016 + 3000, diff);
assert_eq!(diff > new_diff, true);
let new_diff = Difficulty::calculate_req_difficulty(60 * 2016 - 3000, diff);
assert_eq!(diff < new_diff, true);
}

#[test]
fn calc_difficulty_max_min() {
assert_eq!(
Difficulty::min(),
Difficulty::calculate_req_difficulty(0, Difficulty::min()),
);
let diff = Difficulty::from(1000);
let new_diff = Difficulty::calculate_req_difficulty(60 * 2016 * 1000, diff);
assert_eq!(new_diff, Difficulty::from(500));
let new_diff = Difficulty::calculate_req_difficulty(60, diff);
assert_eq!(new_diff, Difficulty::from(1500));
}
}
8 changes: 8 additions & 0 deletions base_layer/core/src/proof_of_work/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ pub enum PowError {
// ProofOfWorkFailed
InvalidProofOfWork,
}

#[derive(Debug, Error, Clone)]
pub enum DifficultyAdjustmentError {
// Accumulated difficulty values can only strictly increase
DecreasingAccumulatedDifficulty,
// Other difficulty algorithm errors
Other,
}
202 changes: 202 additions & 0 deletions base_layer/core/src/proof_of_work/lwma_diff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// LWMA-1 for BTC & Zcash clones
// Copyright (c) 2017-2019 The Bitcoin Gold developers, Zawy, iamstenman (Microbitcoin)
// MIT License
// Algorithm by Zawy, a modification of WT-144 by Tom Harding
// References:
// https://github.com/zawy12/difficulty-algorithms/issues/3#issuecomment-442129791
// https://github.com/zcash/zcash/issues/4021

use crate::proof_of_work::{
difficulty::{Difficulty, DifficultyAdjustment},
error::DifficultyAdjustmentError,
};
use std::{cmp, collections::VecDeque};

// target time of 1 minute
const TARGET: u64 = 60;
// we should cap the amount of entries in the VecDeques to this number.
const BLOCK_WINDOW: usize = 150;
const INITIAL_DIFFICULTY: Difficulty = Difficulty::min();

pub struct LinearWeightedMovingAverage {
timestamps: VecDeque<u64>,
accumulated_difficulties: VecDeque<Difficulty>,
block_window: usize,
}

impl Default for LinearWeightedMovingAverage {
fn default() -> Self {
LinearWeightedMovingAverage::new(BLOCK_WINDOW)
}
}

impl LinearWeightedMovingAverage {
pub fn new(block_window: usize) -> LinearWeightedMovingAverage {
LinearWeightedMovingAverage {
timestamps: VecDeque::with_capacity(block_window + 1),
accumulated_difficulties: VecDeque::with_capacity(block_window + 1),
block_window,
}
}

fn calculate(&self) -> Difficulty {
let timestamps = &self.timestamps;
if timestamps.len() <= 1 {
return INITIAL_DIFFICULTY;
}

// Use the array length rather than block_window to include early cases where the no. of pts < block_window
let n = (timestamps.len() - 1) as u64;

let mut weighted_times: u64 = 0;

let difficulty: u64 = self.accumulated_difficulties[n as usize]
.checked_sub(self.accumulated_difficulties[0])
.expect("Accumulated difficulties cannot decrease in proof of work")
.as_u64();
let ave_difficulty = difficulty as f64 / n as f64;

// Loop through N most recent blocks.
for i in 1..(n + 1) as usize {
// 6*T limit prevents large drops in diff from long solve times which would cause oscillations.
let solve_time = match timestamps[i].checked_sub(timestamps[i - 1]) {
None | Some(0) => 1,
Some(v) => cmp::min(v, 6 * TARGET),
};

// Give linearly higher weight to more recent solve times.
// Note: This will not overflow for practical values of block_window and solve time.
weighted_times += solve_time * i as u64;
}
// k is the sum of weights (1+2+..+n) * target_time
let k = n * (n + 1) * TARGET / 2;
let target = ave_difficulty * k as f64 / weighted_times as f64;
if target > std::u64::MAX as f64 {
panic!("Difficulty target has overflowed");
}
let target = target as u64;
target.into()
}
}

impl DifficultyAdjustment for LinearWeightedMovingAverage {
fn add(&mut self, timestamp: u64, accumulated_difficulty: Difficulty) -> Result<(), DifficultyAdjustmentError> {
match self.accumulated_difficulties.back() {
None => {},
Some(v) => {
if accumulated_difficulty <= *v {
return Err(DifficultyAdjustmentError::DecreasingAccumulatedDifficulty);
}
},
};
self.timestamps.push_back(timestamp);
self.accumulated_difficulties.push_back(accumulated_difficulty);
while self.timestamps.len() > self.block_window + 1 {
self.timestamps.pop_front();
self.accumulated_difficulties.pop_front();
}
Ok(())
}

fn get_difficulty(&self) -> Difficulty {
self.calculate()
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn lwma_zero_len() {
let dif = LinearWeightedMovingAverage::default();
assert_eq!(dif.get_difficulty(), Difficulty::min());
}

#[test]
fn lwma_add_non_increasing_diff() {
let mut dif = LinearWeightedMovingAverage::default();
assert!(dif.add(100, 100.into()).is_ok());
assert!(dif.add(100, 100.into()).is_err());
assert!(dif.add(100, 50.into()).is_err());
}

#[test]
fn lwma_negative_solve_times() {
let mut dif = LinearWeightedMovingAverage::default();
let mut timestamp = 60;
let mut cum_diff = Difficulty::from(100);
let _ = dif.add(timestamp, cum_diff);
timestamp += 60;
cum_diff += Difficulty::from(100);
let _ = dif.add(timestamp, cum_diff);
// Lets create a history and populate the vecs
for _i in 0..150 {
cum_diff += Difficulty::from(100);
timestamp += 60;
let _ = dif.add(timestamp, cum_diff);
}
// lets create chaos by having 60 blocks as negative solve times. This should never be allowed in practive by
// having checks on the block times.
for _i in 0..60 {
cum_diff += Difficulty::from(100);
timestamp -= 1; // Only choosing -1 here since we are testing negative solve times and we cannot have 0 time
let diff_before = dif.get_difficulty();
let _ = dif.add(timestamp, cum_diff);
let diff_after = dif.get_difficulty();
// Algo should handle this as 1sec solve time thus increase the difficulty constantly
assert!(diff_after > diff_before);
}
}

#[test]
fn lwma_limit_difficulty_change() {
let mut dif = LinearWeightedMovingAverage::new(5);
let _ = dif.add(60, 100.into());
let _ = dif.add(10_000_000, 200.into());
assert_eq!(dif.get_difficulty(), 16.into());
let _ = dif.add(20_000_000, 216.into());
assert_eq!(dif.get_difficulty(), 9.into());
}

#[test]
// Data for 5-period moving average
// Timestamp: 60, 120, 180, 240, 300, 350, 380, 445, 515, 615, 975, 976, 977, 978, 979
// Intervals: 60, 60, 60, 60, 60, 50, 30, 65, 70, 100, 360, 1, 1, 1, 1
// Diff: 100, 100, 100, 100, 100, 105, 128, 123, 116, 94, 39, 46, 55, 75, 148
// Acum dif: 100, 200, 300, 400, 500, 605, 733, 856, 972,1066,1105,1151,1206,1281,1429
// Target: 1, 100, 100, 100, 100, 106, 135, 129, 119, 93, 35, 38, 46, 66, 174
fn lwma_calculate() {
let mut dif = LinearWeightedMovingAverage::new(5);
let _ = dif.add(60, 100.into());
assert_eq!(dif.get_difficulty(), 1.into());
let _ = dif.add(120, 200.into());
assert_eq!(dif.get_difficulty(), 100.into());
let _ = dif.add(180, 300.into());
assert_eq!(dif.get_difficulty(), 100.into());
let _ = dif.add(240, 400.into());
assert_eq!(dif.get_difficulty(), 100.into());
let _ = dif.add(300, 500.into());
assert_eq!(dif.get_difficulty(), 100.into());
let _ = dif.add(350, 605.into());
assert_eq!(dif.get_difficulty(), 106.into());
let _ = dif.add(380, 733.into());
assert_eq!(dif.get_difficulty(), 135.into());
let _ = dif.add(445, 856.into());
assert_eq!(dif.get_difficulty(), 129.into());
let _ = dif.add(515, 972.into());
assert_eq!(dif.get_difficulty(), 119.into());
let _ = dif.add(615, 1066.into());
assert_eq!(dif.get_difficulty(), 93.into());
let _ = dif.add(975, 1105.into());
assert_eq!(dif.get_difficulty(), 35.into());
let _ = dif.add(976, 1151.into());
assert_eq!(dif.get_difficulty(), 38.into());
let _ = dif.add(977, 1206.into());
assert_eq!(dif.get_difficulty(), 46.into());
let _ = dif.add(978, 1281.into());
assert_eq!(dif.get_difficulty(), 66.into());
let _ = dif.add(979, 1429.into());
assert_eq!(dif.get_difficulty(), 174.into());
}
}
2 changes: 2 additions & 0 deletions base_layer/core/src/proof_of_work/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ mod difficulty;
mod error;
mod pow;

pub mod lwma_diff;

pub use blake_pow::BlakePow;
pub use difficulty::Difficulty;
pub use error::PowError;
Expand Down