Skip to content

Commit

Permalink
Add LWMA difficulty adjustment algorithm
Browse files Browse the repository at this point in the history
This PR adds in a difficulty adjustment algorithm.

Motivation and Context

We need to adjust the difficulty per block. This PR adds the LWMA
algorithm as per zawy12/difficulty-algorithms#3
  • Loading branch information
CjS77 committed Nov 6, 2019
1 parent ee2b3d3 commit 27f0f61
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 51 deletions.
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
68 changes: 18 additions & 50 deletions base_layer/core/src/proof_of_work/difficulty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,42 +28,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 +87,16 @@ 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
fn add(&mut self, timestamp: u64, accumulated_difficulty: Difficulty);

/// 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 +110,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));
}
}
182 changes: 182 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,182 @@
// 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::{self, Difficulty, DifficultyAdjustment};
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) {
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();
}
}

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_negative_solve_times() {
let mut dif = LinearWeightedMovingAverage::default();
let mut timestamp = 60;
let mut cum_diff = Difficulty::from(100);
dif.add(timestamp, cum_diff);
timestamp += 60;
cum_diff += Difficulty::from(100);
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;
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();
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);
dif.add(60, 100.into());
dif.add(10_000_000, 200.into());
assert_eq!(dif.get_difficulty(), 16.into());
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);
dif.add(60, 100.into());
assert_eq!(dif.get_difficulty(), 1.into());
dif.add(120, 200.into());
assert_eq!(dif.get_difficulty(), 100.into());
dif.add(180, 300.into());
assert_eq!(dif.get_difficulty(), 100.into());
dif.add(240, 400.into());
assert_eq!(dif.get_difficulty(), 100.into());
dif.add(300, 500.into());
assert_eq!(dif.get_difficulty(), 100.into());
dif.add(350, 605.into());
assert_eq!(dif.get_difficulty(), 106.into());
dif.add(380, 733.into());
assert_eq!(dif.get_difficulty(), 135.into());
dif.add(445, 856.into());
assert_eq!(dif.get_difficulty(), 129.into());
dif.add(515, 972.into());
assert_eq!(dif.get_difficulty(), 119.into());
dif.add(615, 1066.into());
assert_eq!(dif.get_difficulty(), 93.into());
dif.add(975, 1105.into());
assert_eq!(dif.get_difficulty(), 35.into());
dif.add(976, 1151.into());
assert_eq!(dif.get_difficulty(), 38.into());
dif.add(977, 1206.into());
assert_eq!(dif.get_difficulty(), 46.into());
dif.add(978, 1281.into());
assert_eq!(dif.get_difficulty(), 66.into());
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

0 comments on commit 27f0f61

Please sign in to comment.