diff --git a/core/src/consensus.rs b/core/src/consensus.rs index cc95d6c73..29d2cb98b 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -32,7 +32,7 @@ pub const REWARD: u64 = 1_000_000_000; /// that we may reduce this value in the future as we get more data on mining /// with Cuckoo Cycle, networks improve and block propagation is optimized /// (adjusting the reward accordingly). -pub const BLOCK_TIME_SEC: u8 = 60; +pub const BLOCK_TIME_SEC: i64 = 60; /// Cuckoo-cycle proof size (cycle length) pub const PROOFSIZE: usize = 42; @@ -53,6 +53,94 @@ pub const MAX_SIZESHIFT: u8 = 29; /// a solution. pub const EASINESS: u32 = 50; +/// Default number of blocks in the past when cross-block cut-through will start +/// happening. Needs to be long enough to not overlap with a long reorg. +/// Rational +/// behind the value is the longest bitcoin fork was about 30 blocks, so 5h. We +/// add an order of magnitude to be safe and round to 48h of blocks to make it +/// easier to reason about. +pub const CUT_THROUGH_HORIZON: u32 = 48 * 3600 / (BLOCK_TIME_SEC as u32); + +/// The maximum size we're willing to accept for any message. Enforced by the +/// peer-to-peer networking layer only for DoS protection. +pub const MAX_MSG_LEN: u64 = 20_000_000; + +pub const MEDIAN_TIME_WINDOW: u32 = 11; + +pub const DIFFICULTY_ADJUST_WINDOW: u32 = 23; + +pub const BLOCK_TIME_WINDOW: i64 = (DIFFICULTY_ADJUST_WINDOW as i64) * BLOCK_TIME_SEC; + +pub const UPPER_TIME_BOUND: i64 = BLOCK_TIME_WINDOW * 4 / 3; + +pub const LOWER_TIME_BOUND: i64 = BLOCK_TIME_WINDOW * 5 / 6; + +#[derive(Debug, Clone)] +pub struct TargetError { + err: String, +} +pub fn next_target2(cursor: T) -> Result + where T: IntoIterator> +{ + + // Block times at the begining and end of the adjustment window, used to + // calculate medians later. + let mut window_begin = vec![]; + let mut window_end = vec![]; + + // Sum of difficulties in the window, used to calculate the average later. + let mut diff_sum = Difficulty::zero(); + + // Enumerating backward over blocks + for (n, head_info) in cursor.into_iter().enumerate() { + let m = n as u32; + let (ts, diff) = head_info?; + + // Sum each element in the adjustment window. In addition, retain + // timestamps within median windows (at ]start;start-11] and ]end;end-11] + // to later calculate medians. + if m < DIFFICULTY_ADJUST_WINDOW { + diff_sum = diff_sum + diff; + + if m < MEDIAN_TIME_WINDOW { + window_begin.push(ts); + } + } else if m < DIFFICULTY_ADJUST_WINDOW + MEDIAN_TIME_WINDOW { + window_end.push(ts); + } else { + break; + } + } + + // Check we have enough blocks + if window_end.len() < (MEDIAN_TIME_WINDOW as usize) { + return Ok(Difficulty::one()); + } + + // Calculating time medians at the beginning and end of the window. + window_begin.sort(); + window_end.sort(); + let begin_ts = window_begin[window_begin.len() / 2]; + let end_ts = window_end[window_end.len() / 2]; + + // Average difficulty and dampened average time + let diff_avg = diff_sum / Difficulty::from_num(DIFFICULTY_ADJUST_WINDOW); + let ts_damp = (3 * BLOCK_TIME_WINDOW + (begin_ts - end_ts)) / 4; + + // Apply time bounds + let adj_ts = if ts_damp < LOWER_TIME_BOUND { + LOWER_TIME_BOUND + } else if ts_damp > UPPER_TIME_BOUND { + UPPER_TIME_BOUND + } else { + ts_damp + }; + + // Final ratio calculation + Ok(diff_avg * Difficulty::from_num(BLOCK_TIME_WINDOW as u32) / + Difficulty::from_num(adj_ts as u32)) +} + /// Difficulty adjustment somewhat inspired by Ethereum's. Tuned to add or /// remove 1/1024th of the target for each 10 seconds of deviation from the 30 /// seconds block time. Increases Cuckoo size shift by one when next_target @@ -93,60 +181,79 @@ pub fn next_target(ts: i64, } } -/// Default number of blocks in the past when cross-block cut-through will start -/// happening. Needs to be long enough to not overlap with a long reorg. -/// Rational -/// behind the value is the longest bitcoin fork was about 30 blocks, so 5h. We -/// add an order of magnitude to be safe and round to 48h of blocks to make it -/// easier to reason about. -pub const CUT_THROUGH_HORIZON: u32 = 48 * 3600 / (BLOCK_TIME_SEC as u32); - -/// The maximum size we're willing to accept for any message. Enforced by the -/// peer-to-peer networking layer only for DoS protection. -pub const MAX_MSG_LEN: u64 = 20_000_000; - #[cfg(test)] mod test { use core::target::Difficulty; use super::*; - #[test] - /// Checks different next_target adjustments and difficulty boundaries - fn next_target_adjustment() { - // can't do lower than min - assert_eq!(next_target(60, 0, Difficulty::one(), 26), - (Difficulty::one(), 26)); - assert_eq!(next_target(90, 30, Difficulty::one(), 26), - (Difficulty::one(), 26)); - assert_eq!(next_target(60, 0, Difficulty::one(), 26), - (Difficulty::one(), 26)); - - // lower next_target if gap too short - assert_eq!(next_target(30, 0, Difficulty::one(), 26).0, - Difficulty::from_num(4)); - assert_eq!(next_target(50, 0, Difficulty::one(), 26).0, - Difficulty::from_num(2)); - assert_eq!(next_target(40, 0, Difficulty::from_num(1024 * 8), 26).0, - Difficulty::from_num(1024 * 8 + 18)); - - // lower difficulty if gap too wide - assert_eq!(next_target(70, 0, Difficulty::from_num(10), 26).0, - Difficulty::from_num(9)); - assert_eq!(next_target(90, 0, Difficulty::from_num(1024 * 8), 26).0, - Difficulty::from_num(1024 * 8 - 9 * 3)); - - // identical, no adjustment - assert_eq!(next_target(60, 0, Difficulty::from_num(1024 * 8), 26).0, - Difficulty::from_num(1024 * 8)); - - // increase cuckoo size if next_target goes above soft max, target is doubled, - // up to 29 - assert_eq!(next_target(60, 0, Difficulty::from_num(1 << 16), 25), - (Difficulty::from_num(1 << 16), 25)); - assert_eq!(next_target(60, 0, Difficulty::from_num((1 << 16) + 1), 25), - (Difficulty::from_num(1 << 15), 26)); - assert_eq!(next_target(60, 0, Difficulty::from_num((1 << 24) + 1), 26), - (Difficulty::from_num(1 << 23), 27)); + // Builds an iterator for next difficulty calculation with the provided + // constant time interval, difficulty and total length. + fn repeat(interval: i64, diff: u32, len: u32) -> Vec> { + let diffs = vec![Difficulty::from_num(diff); len as usize]; + let times = (0..(len as usize)).map(|n| (n as i64) * interval).rev(); + let pairs = times.zip(diffs.iter()); + pairs.map(|(t, d)| Ok((t, d.clone()))).collect::>() } + + fn repeat_offs(from: i64, + interval: i64, + diff: u32, + len: u32) + -> Vec> { + map_vec!(repeat(interval, diff, len), |e| { + match e.clone() { + Err(e) => Err(e), + Ok((t, d)) => Ok((t + from, d)), + } + }) + } + + /// Checks different next_target adjustments and difficulty boundaries + #[test] + fn next_target_adjustment() { + // not enough data + assert_eq!(next_target2(vec![]).unwrap(), Difficulty::one()); + assert_eq!(next_target2(vec![Ok((60, Difficulty::one()))]).unwrap(), + Difficulty::one()); + assert_eq!(next_target2(repeat(60, 10, DIFFICULTY_ADJUST_WINDOW)).unwrap(), + Difficulty::one()); + + // just enough data, right interval, should stay constant + let just_enough = DIFFICULTY_ADJUST_WINDOW + MEDIAN_TIME_WINDOW; + assert_eq!(next_target2(repeat(60, 1000, just_enough)).unwrap(), + Difficulty::from_num(1000)); + + // checking averaging works, window length is odd so need to compensate a little + let sec = DIFFICULTY_ADJUST_WINDOW / 2 + 1 + MEDIAN_TIME_WINDOW; + let mut s1 = repeat(60, 500, sec); + let mut s2 = repeat_offs((sec * 60) as i64, 60, 1545, DIFFICULTY_ADJUST_WINDOW / 2); + s2.append(&mut s1); + assert_eq!(next_target2(s2).unwrap(), Difficulty::from_num(999)); + + // too slow, diff goes down + assert_eq!(next_target2(repeat(90, 1000, just_enough)).unwrap(), + Difficulty::from_num(889)); + assert_eq!(next_target2(repeat(120, 1000, just_enough)).unwrap(), + Difficulty::from_num(800)); + + // too fast, diff goes up + assert_eq!(next_target2(repeat(55, 1000, just_enough)).unwrap(), + Difficulty::from_num(1021)); + assert_eq!(next_target2(repeat(45, 1000, just_enough)).unwrap(), + Difficulty::from_num(1067)); + + // hitting lower time bound, should always get the same result below + assert_eq!(next_target2(repeat(20, 1000, just_enough)).unwrap(), + Difficulty::from_num(1200)); + assert_eq!(next_target2(repeat(10, 1000, just_enough)).unwrap(), + Difficulty::from_num(1200)); + + // hitting higher time bound, should always get the same result above + assert_eq!(next_target2(repeat(160, 1000, just_enough)).unwrap(), + Difficulty::from_num(750)); + assert_eq!(next_target2(repeat(200, 1000, just_enough)).unwrap(), + Difficulty::from_num(750)); + } + } diff --git a/core/src/core/target.rs b/core/src/core/target.rs index 88b2ab2cc..bbbb2b6d6 100644 --- a/core/src/core/target.rs +++ b/core/src/core/target.rs @@ -17,7 +17,7 @@ //! the related difficulty, defined as the maximum target divided by the hash. use std::fmt; -use std::ops::Add; +use std::ops::{Add, Mul, Div}; use bigint::BigUint; use serde::{Serialize, Serializer, Deserialize, Deserializer, de}; @@ -37,9 +37,14 @@ pub struct Difficulty { } impl Difficulty { + /// Difficulty of zero, which is practically invalid (not target can be + /// calculated from it) but very useful as a start for additions. + pub fn zero() -> Difficulty { + Difficulty { num: BigUint::new(vec![0]) } + } + /// Difficulty of one, which is the minumum difficulty (when the hash - /// equals the - /// max target) + /// equals the max target) pub fn one() -> Difficulty { Difficulty { num: BigUint::new(vec![1]) } } @@ -81,6 +86,20 @@ impl Add for Difficulty { } } +impl Mul for Difficulty { + type Output = Difficulty; + fn mul(self, other: Difficulty) -> Difficulty { + Difficulty { num: self.num * other.num } + } +} + +impl Div for Difficulty { + type Output = Difficulty; + fn div(self, other: Difficulty) -> Difficulty { + Difficulty { num: self.num / other.num } + } +} + impl Writeable for Difficulty { fn write(&self, writer: &mut W) -> Result<(), ser::Error> { let data = self.num.to_bytes_be();