diff --git a/core/tests/consensus.rs b/core/tests/consensus.rs deleted file mode 100644 index 3ad061cb..00000000 --- a/core/tests/consensus.rs +++ /dev/null @@ -1,711 +0,0 @@ -// Copyright 2020 The Grin Developers -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! core consensus.rs tests (separated to de-clutter consensus.rs) -use grin_core as core; - -use self::core::consensus::*; -use self::core::core::block::HeaderVersion; -use self::core::global; -use self::core::pow::Difficulty; -use chrono::prelude::Utc; -use std::fmt::{self, Display}; - -/// Last n blocks for difficulty calculation purposes -/// (copied from stats in server crate) -#[derive(Clone, Debug)] -pub struct DiffBlock { - /// Block number (can be negative for a new chain) - pub block_number: i64, - /// Block network difficulty - pub difficulty: u64, - /// Time block was found (epoch seconds) - pub time: u64, - /// Duration since previous block (epoch seconds) - pub duration: u64, -} - -/// Stats on the last WINDOW blocks and the difficulty calculation -/// (Copied from stats in server crate) -#[derive(Clone)] -pub struct DiffStats { - /// latest height - pub height: u64, - /// Last WINDOW block data - pub last_blocks: Vec, - /// Average block time for last WINDOW blocks - pub average_block_time: u64, - /// Average WINDOW difficulty - pub average_difficulty: u64, - /// WINDOW size - pub window_size: u64, - /// Block time sum - pub block_time_sum: u64, - /// Block diff sum - pub block_diff_sum: u64, - /// latest ts - pub latest_ts: u64, - /// earliest ts - pub earliest_ts: u64, - /// ts delta - pub ts_delta: u64, -} - -impl Display for DiffBlock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let output = format!( - "Block Number: {} Difficulty: {}, Time: {}, Duration: {}", - self.block_number, self.difficulty, self.time, self.duration - ); - Display::fmt(&output, f) - } -} - -// Builds an iterator for next difficulty calculation with the provided -// constant time interval, difficulty and total length. -fn repeat(interval: u64, diff: HeaderInfo, len: u64, cur_time: Option) -> Vec { - let cur_time = match cur_time { - Some(t) => t, - None => Utc::now().timestamp() as u64, - }; - // watch overflow here, length shouldn't be ridiculous anyhow - assert!(len < std::usize::MAX as u64); - let diffs = vec![diff.difficulty.clone(); len as usize]; - let times = (0..(len as usize)).map(|n| n * interval as usize).rev(); - let pairs = times.zip(diffs.iter()); - pairs - .map(|(t, d)| { - HeaderInfo::new( - diff.block_hash, - cur_time + t as u64, - d.clone(), - diff.secondary_scaling, - diff.is_secondary, - ) - }) - .collect::>() -} - -// Creates a new chain with a genesis at a simulated difficulty -fn create_chain_sim(diff: u64) -> Vec<(HeaderInfo, DiffStats)> { - println!( - "adding create: {}, {}", - Utc::now().timestamp(), - Difficulty::from_num(diff) - ); - let return_vec = vec![HeaderInfo::from_ts_diff( - Utc::now().timestamp() as u64, - Difficulty::from_num(diff), - )]; - let diff_stats = get_diff_stats(&return_vec); - vec![( - HeaderInfo::from_ts_diff(Utc::now().timestamp() as u64, Difficulty::from_num(diff)), - diff_stats, - )] -} - -fn get_diff_stats(chain_sim: &Vec) -> DiffStats { - // Fill out some difficulty stats for convenience - let diff_iter = chain_sim.clone(); - let last_blocks: Vec = global::difficulty_data_to_vector(diff_iter.iter().cloned()); - - let mut last_time = last_blocks[0].timestamp; - let tip_height = chain_sim.len(); - let earliest_block_height = tip_height as i64 - last_blocks.len() as i64; - - let earliest_ts = last_blocks[0].timestamp; - let latest_ts = last_blocks[last_blocks.len() - 1].timestamp; - - let mut i = 1; - - let sum_blocks: Vec = global::difficulty_data_to_vector(diff_iter.iter().cloned()) - .into_iter() - .take(DIFFICULTY_ADJUST_WINDOW as usize) - .collect(); - - let sum_entries: Vec = sum_blocks - .iter() - //.skip(1) - .map(|n| { - let dur = n.timestamp - last_time; - let height = earliest_block_height + i + 1; - i += 1; - last_time = n.timestamp; - DiffBlock { - block_number: height, - difficulty: n.difficulty.to_num(), - time: n.timestamp, - duration: dur, - } - }) - .collect(); - - let block_time_sum = sum_entries.iter().fold(0, |sum, t| sum + t.duration); - let block_diff_sum = sum_entries.iter().fold(0, |sum, d| sum + d.difficulty); - - i = 1; - last_time = last_blocks[0].clone().timestamp; - - let diff_entries: Vec = last_blocks - .iter() - .skip(1) - .map(|n| { - let dur = n.timestamp - last_time; - let height = earliest_block_height + i; - i += 1; - last_time = n.timestamp; - DiffBlock { - block_number: height, - difficulty: n.difficulty.to_num(), - time: n.timestamp, - duration: dur, - } - }) - .collect(); - - DiffStats { - height: tip_height as u64, - last_blocks: diff_entries, - average_block_time: block_time_sum / (DIFFICULTY_ADJUST_WINDOW), - average_difficulty: block_diff_sum / (DIFFICULTY_ADJUST_WINDOW), - window_size: DIFFICULTY_ADJUST_WINDOW, - block_time_sum: block_time_sum, - block_diff_sum: block_diff_sum, - latest_ts: latest_ts, - earliest_ts: earliest_ts, - ts_delta: latest_ts - earliest_ts, - } -} - -// Adds another 'block' to the iterator, so to speak, with difficulty calculated -// from the difficulty adjustment at interval seconds from the previous block -fn add_block( - interval: u64, - chain_sim: Vec<(HeaderInfo, DiffStats)>, -) -> Vec<(HeaderInfo, DiffStats)> { - let mut ret_chain_sim = chain_sim.clone(); - let mut return_chain: Vec = chain_sim.clone().iter().map(|e| e.0.clone()).collect(); - // get last interval - let diff = next_difficulty(1, return_chain.clone()); - let last_elem = chain_sim.first().unwrap().clone().0; - let time = last_elem.timestamp + interval; - return_chain.insert(0, HeaderInfo::from_ts_diff(time, diff.difficulty)); - let diff_stats = get_diff_stats(&return_chain); - ret_chain_sim.insert( - 0, - (HeaderInfo::from_ts_diff(time, diff.difficulty), diff_stats), - ); - ret_chain_sim -} - -// Adds another n 'blocks' to the iterator, with difficulty calculated -fn add_block_repeated( - interval: u64, - chain_sim: Vec<(HeaderInfo, DiffStats)>, - iterations: usize, -) -> Vec<(HeaderInfo, DiffStats)> { - let mut return_chain = chain_sim.clone(); - for _ in 0..iterations { - return_chain = add_block(interval, return_chain.clone()); - } - return_chain -} - -// Prints the contents of the iterator and its difficulties.. useful for -// tweaking -fn print_chain_sim(chain_sim: Vec<(HeaderInfo, DiffStats)>) { - let mut chain_sim = chain_sim.clone(); - chain_sim.reverse(); - let mut last_time = 0; - let mut first = true; - println!("Constants"); - println!("DIFFICULTY_ADJUST_WINDOW: {}", DIFFICULTY_ADJUST_WINDOW); - println!("BLOCK_TIME_WINDOW: {}", BLOCK_TIME_WINDOW); - println!("CLAMP_FACTOR: {}", CLAMP_FACTOR); - println!("DAMP_FACTOR: {}", DIFFICULTY_DAMP_FACTOR); - chain_sim.iter().enumerate().for_each(|(i, b)| { - let block = b.0.clone(); - let stats = b.1.clone(); - if first { - last_time = block.timestamp; - first = false; - } - println!( - "Height: {}, Time: {}, Interval: {}, Network difficulty:{}, Average Block Time: {}, Average Difficulty {}, Block Time Sum: {}, Block Diff Sum: {}, Latest Timestamp: {}, Earliest Timestamp: {}, Timestamp Delta: {}", - i, - block.timestamp, - block.timestamp - last_time, - block.difficulty, - stats.average_block_time, - stats.average_difficulty, - stats.block_time_sum, - stats.block_diff_sum, - stats.latest_ts, - stats.earliest_ts, - stats.ts_delta, - ); - let mut sb = stats.last_blocks.clone(); - sb.reverse(); - for i in sb { - println!(" {}", i); - } - last_time = block.timestamp; - }); -} - -fn repeat_offs(from: u64, interval: u64, diff: u64, len: u64) -> Vec { - repeat( - interval, - HeaderInfo::from_ts_diff(1, Difficulty::from_num(diff)), - len, - Some(from), - ) -} - -/// Checks different next_target adjustments and difficulty boundaries -#[test] -fn adjustment_scenarios() { - // Use production parameters for genesis diff - global::set_mining_mode(global::ChainTypes::Mainnet); - - // Genesis block with initial diff - let chain_sim = create_chain_sim(global::initial_block_difficulty()); - // Scenario 1) Hash power is massively over estimated, first block takes an hour - let chain_sim = add_block_repeated(3600, chain_sim, 2); - let chain_sim = add_block_repeated(1800, chain_sim, 2); - let chain_sim = add_block_repeated(900, chain_sim, 10); - let chain_sim = add_block_repeated(450, chain_sim, 30); - let chain_sim = add_block_repeated(400, chain_sim, 30); - let chain_sim = add_block_repeated(300, chain_sim, 30); - - println!("*********************************************************"); - println!("Scenario 1) Grossly over-estimated genesis difficulty "); - println!("*********************************************************"); - print_chain_sim(chain_sim); - println!("*********************************************************"); - - // Under-estimated difficulty - let chain_sim = create_chain_sim(global::initial_block_difficulty()); - let chain_sim = add_block_repeated(1, chain_sim, 5); - let chain_sim = add_block_repeated(20, chain_sim, 5); - let chain_sim = add_block_repeated(30, chain_sim, 20); - - println!("*********************************************************"); - println!("Scenario 2) Grossly under-estimated genesis difficulty "); - println!("*********************************************************"); - print_chain_sim(chain_sim); - println!("*********************************************************"); - let just_enough = (DIFFICULTY_ADJUST_WINDOW) as usize; - - // Steady difficulty for a good while, then a sudden drop - let chain_sim = create_chain_sim(global::initial_block_difficulty()); - let chain_sim = add_block_repeated(60, chain_sim, just_enough as usize); - let chain_sim = add_block_repeated(600, chain_sim, 60); - - println!(""); - println!("*********************************************************"); - println!("Scenario 3) Sudden drop in hashpower"); - println!("*********************************************************"); - print_chain_sim(chain_sim); - println!("*********************************************************"); - - // Sudden increase - let chain_sim = create_chain_sim(global::initial_block_difficulty()); - let chain_sim = add_block_repeated(60, chain_sim, just_enough as usize); - let chain_sim = add_block_repeated(10, chain_sim, 10); - - println!(""); - println!("*********************************************************"); - println!("Scenario 4) Sudden increase in hashpower"); - println!("*********************************************************"); - print_chain_sim(chain_sim); - println!("*********************************************************"); - - // Oscillations - let chain_sim = create_chain_sim(global::initial_block_difficulty()); - let chain_sim = add_block_repeated(60, chain_sim, just_enough as usize); - let chain_sim = add_block_repeated(10, chain_sim, 10); - let chain_sim = add_block_repeated(60, chain_sim, 20); - let chain_sim = add_block_repeated(10, chain_sim, 10); - - println!(""); - println!("*********************************************************"); - println!("Scenario 5) Oscillations in hashpower"); - println!("*********************************************************"); - print_chain_sim(chain_sim); - println!("*********************************************************"); -} - -/// Checks different next_target adjustments and difficulty boundaries -#[test] -fn next_target_adjustment() { - global::set_mining_mode(global::ChainTypes::AutomatedTesting); - let cur_time = Utc::now().timestamp() as u64; - let diff_min = Difficulty::min(); - - // Check we don't get stuck on difficulty <= MIN_DIFFICULTY (at 4x faster blocks at least) - let mut hi = HeaderInfo::from_diff_scaling(diff_min, AR_SCALE_DAMP_FACTOR as u32); - hi.is_secondary = false; - let hinext = next_difficulty( - 1, - repeat( - BLOCK_TIME_SEC / 4, - hi.clone(), - DIFFICULTY_ADJUST_WINDOW, - None, - ), - ); - - assert_ne!(hinext.difficulty, diff_min); - - // Check we don't get stuck on scale MIN_DIFFICULTY, when primary frequency is too high - assert_ne!(hinext.secondary_scaling, MIN_DIFFICULTY as u32); - - // just enough data, right interval, should stay constant - let just_enough = DIFFICULTY_ADJUST_WINDOW + 1; - hi.difficulty = Difficulty::from_num(10000); - assert_eq!( - next_difficulty(1, repeat(BLOCK_TIME_SEC, hi.clone(), just_enough, None)).difficulty, - Difficulty::from_num(10000) - ); - - // check pre difficulty_data_to_vector effect on retargetting - assert_eq!( - next_difficulty(1, vec![HeaderInfo::from_ts_diff(42, hi.difficulty)]).difficulty, - Difficulty::from_num(14913) - ); - - // checking averaging works - hi.difficulty = Difficulty::from_num(500); - let sec = DIFFICULTY_ADJUST_WINDOW / 2; - let mut s1 = repeat(BLOCK_TIME_SEC, hi.clone(), sec, Some(cur_time)); - let mut s2 = repeat_offs( - cur_time + (sec * BLOCK_TIME_SEC) as u64, - BLOCK_TIME_SEC, - 1500, - DIFFICULTY_ADJUST_WINDOW / 2, - ); - s2.append(&mut s1); - assert_eq!( - next_difficulty(1, s2).difficulty, - Difficulty::from_num(1000) - ); - - // too slow, diff goes down - hi.difficulty = Difficulty::from_num(1000); - assert_eq!( - next_difficulty(1, repeat(90, hi.clone(), just_enough, None)).difficulty, - Difficulty::from_num(857) - ); - assert_eq!( - next_difficulty(1, repeat(120, hi.clone(), just_enough, None)).difficulty, - Difficulty::from_num(750) - ); - - // too fast, diff goes up - assert_eq!( - next_difficulty(1, repeat(55, hi.clone(), just_enough, None)).difficulty, - Difficulty::from_num(1028) - ); - assert_eq!( - next_difficulty(1, repeat(45, hi.clone(), just_enough, None)).difficulty, - Difficulty::from_num(1090) - ); - assert_eq!( - next_difficulty(1, repeat(30, hi.clone(), just_enough, None)).difficulty, - Difficulty::from_num(1200) - ); - - // hitting lower time bound, should always get the same result below - assert_eq!( - next_difficulty(1, repeat(0, hi.clone(), just_enough, None)).difficulty, - Difficulty::from_num(1500) - ); - - // hitting higher time bound, should always get the same result above - assert_eq!( - next_difficulty(1, repeat(300, hi.clone(), just_enough, None)).difficulty, - Difficulty::from_num(500) - ); - assert_eq!( - next_difficulty(1, repeat(400, hi.clone(), just_enough, None)).difficulty, - Difficulty::from_num(500) - ); - - // We should never drop below minimum - hi.difficulty = Difficulty::zero(); - assert_eq!( - next_difficulty(1, repeat(90, hi.clone(), just_enough, None)).difficulty, - Difficulty::min() - ); -} - -#[test] -fn test_secondary_pow_ratio() { - // Tests for mainnet chain type. - { - global::set_mining_mode(global::ChainTypes::Mainnet); - assert_eq!(global::is_floonet(), false); - - assert_eq!(secondary_pow_ratio(1), 90); - assert_eq!(secondary_pow_ratio(89), 90); - assert_eq!(secondary_pow_ratio(90), 90); - assert_eq!(secondary_pow_ratio(91), 90); - assert_eq!(secondary_pow_ratio(179), 90); - assert_eq!(secondary_pow_ratio(180), 90); - assert_eq!(secondary_pow_ratio(181), 90); - - let one_week = 60 * 24 * 7; - assert_eq!(secondary_pow_ratio(one_week - 1), 90); - assert_eq!(secondary_pow_ratio(one_week), 90); - assert_eq!(secondary_pow_ratio(one_week + 1), 90); - - let two_weeks = one_week * 2; - assert_eq!(secondary_pow_ratio(two_weeks - 1), 89); - assert_eq!(secondary_pow_ratio(two_weeks), 89); - assert_eq!(secondary_pow_ratio(two_weeks + 1), 89); - - let t4_fork_height = 64_000; - assert_eq!(secondary_pow_ratio(t4_fork_height - 1), 85); - assert_eq!(secondary_pow_ratio(t4_fork_height), 85); - assert_eq!(secondary_pow_ratio(t4_fork_height + 1), 85); - - let one_year = one_week * 52; - assert_eq!(secondary_pow_ratio(one_year), 45); - - let ninety_one_weeks = one_week * 91; - assert_eq!(secondary_pow_ratio(ninety_one_weeks - 1), 12); - assert_eq!(secondary_pow_ratio(ninety_one_weeks), 12); - assert_eq!(secondary_pow_ratio(ninety_one_weeks + 1), 12); - - let two_year = one_year * 2; - assert_eq!(secondary_pow_ratio(two_year - 1), 1); - assert_eq!(secondary_pow_ratio(two_year), 0); - assert_eq!(secondary_pow_ratio(two_year + 1), 0); - } - - // Tests for testnet4 chain type (covers pre and post hardfork). - { - global::set_mining_mode(global::ChainTypes::Floonet); - assert_eq!(global::is_floonet(), true); - - assert_eq!(secondary_pow_ratio(1), 90); - assert_eq!(secondary_pow_ratio(89), 90); - assert_eq!(secondary_pow_ratio(90), 90); - assert_eq!(secondary_pow_ratio(91), 90); - assert_eq!(secondary_pow_ratio(179), 90); - assert_eq!(secondary_pow_ratio(180), 90); - assert_eq!(secondary_pow_ratio(181), 90); - - let one_week = 60 * 24 * 7; - assert_eq!(secondary_pow_ratio(one_week - 1), 90); - assert_eq!(secondary_pow_ratio(one_week), 90); - assert_eq!(secondary_pow_ratio(one_week + 1), 90); - - let two_weeks = one_week * 2; - assert_eq!(secondary_pow_ratio(two_weeks - 1), 89); - assert_eq!(secondary_pow_ratio(two_weeks), 89); - assert_eq!(secondary_pow_ratio(two_weeks + 1), 89); - - let t4_fork_height = 64_000; - assert_eq!(secondary_pow_ratio(t4_fork_height - 1), 85); - assert_eq!(secondary_pow_ratio(t4_fork_height), 85); - assert_eq!(secondary_pow_ratio(t4_fork_height + 1), 85); - - let one_year = one_week * 52; - assert_eq!(secondary_pow_ratio(one_year), 45); - - let ninety_one_weeks = one_week * 91; - assert_eq!(secondary_pow_ratio(ninety_one_weeks - 1), 12); - assert_eq!(secondary_pow_ratio(ninety_one_weeks), 12); - assert_eq!(secondary_pow_ratio(ninety_one_weeks + 1), 12); - - let two_year = one_year * 2; - assert_eq!(secondary_pow_ratio(two_year - 1), 1); - assert_eq!(secondary_pow_ratio(two_year), 0); - assert_eq!(secondary_pow_ratio(two_year + 1), 0); - } -} - -#[test] -fn test_secondary_pow_scale() { - let window = DIFFICULTY_ADJUST_WINDOW; - let mut hi = HeaderInfo::from_diff_scaling(Difficulty::from_num(10), 100); - - // mainnet testing - { - global::set_mining_mode(global::ChainTypes::Mainnet); - assert_eq!(global::is_floonet(), false); - - // all primary, factor should increase so it becomes easier to find a high - // difficulty block - hi.is_secondary = false; - assert_eq!( - secondary_pow_scaling(1, &(0..window).map(|_| hi.clone()).collect::>()), - 108 - ); - // all secondary on 90%, factor should go down a bit - hi.is_secondary = true; - assert_eq!( - secondary_pow_scaling(1, &(0..window).map(|_| hi.clone()).collect::>()), - 99 - ); - // all secondary on 1%, factor should go down to bound (divide by 2) - assert_eq!( - secondary_pow_scaling( - 2 * YEAR_HEIGHT * 83 / 90, - &(0..window).map(|_| hi.clone()).collect::>() - ), - 50 - ); - // same as above, testing lowest bound - let mut low_hi = - HeaderInfo::from_diff_scaling(Difficulty::from_num(10), MIN_AR_SCALE as u32); - low_hi.is_secondary = true; - assert_eq!( - secondary_pow_scaling( - 2 * YEAR_HEIGHT, - &(0..window).map(|_| low_hi.clone()).collect::>() - ), - MIN_AR_SCALE as u32 - ); - // the right ratio of 95% secondary - let mut primary_hi = HeaderInfo::from_diff_scaling(Difficulty::from_num(10), 50); - primary_hi.is_secondary = false; - assert_eq!( - secondary_pow_scaling( - 1, - &(0..(window / 10)) - .map(|_| primary_hi.clone()) - .chain((0..(window * 9 / 10)).map(|_| hi.clone())) - .collect::>() - ), - 95, // avg ar_scale of 10% * 50 + 90% * 100 - ); - // 95% secondary, should come down based on 97.5 average - assert_eq!( - secondary_pow_scaling( - 1, - &(0..(window / 20)) - .map(|_| primary_hi.clone()) - .chain((0..(window * 95 / 100)).map(|_| hi.clone())) - .collect::>() - ), - 97 - ); - // 40% secondary, should come up based on 70 average - assert_eq!( - secondary_pow_scaling( - 1, - &(0..(window * 6 / 10)) - .map(|_| primary_hi.clone()) - .chain((0..(window * 4 / 10)).map(|_| hi.clone())) - .collect::>() - ), - 73 - ); - } -} - -#[test] -fn hard_forks() { - // Tests for mainnet chain type. - { - global::set_mining_mode(global::ChainTypes::Mainnet); - assert_eq!(global::is_floonet(), false); - assert!(valid_header_version(0, HeaderVersion(1))); - assert!(valid_header_version(10, HeaderVersion(1))); - assert!(!valid_header_version(10, HeaderVersion(2))); - assert!(valid_header_version(YEAR_HEIGHT / 2 - 1, HeaderVersion(1))); - assert!(valid_header_version(YEAR_HEIGHT / 2, HeaderVersion(2))); - assert!(valid_header_version(YEAR_HEIGHT / 2 + 1, HeaderVersion(2))); - assert!(!valid_header_version(YEAR_HEIGHT / 2, HeaderVersion(1))); - assert!(!valid_header_version(YEAR_HEIGHT, HeaderVersion(1))); - - assert!(valid_header_version(YEAR_HEIGHT - 1, HeaderVersion(2))); - assert!(valid_header_version(YEAR_HEIGHT, HeaderVersion(3))); - assert!(valid_header_version(YEAR_HEIGHT + 1, HeaderVersion(3))); - assert!(!valid_header_version(YEAR_HEIGHT, HeaderVersion(2))); - assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(2))); - // v4 not active yet - assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(4))); - assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(3))); - assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(2))); - assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(1))); - assert!(!valid_header_version(YEAR_HEIGHT * 2, HeaderVersion(3))); - assert!(!valid_header_version( - YEAR_HEIGHT * 3 / 2 + 1, - HeaderVersion(3) - )); - } - // Tests for floonet chain type. - { - global::set_mining_mode(global::ChainTypes::Floonet); - assert_eq!(global::is_floonet(), true); - assert!(valid_header_version(0, HeaderVersion(1))); - assert!(valid_header_version(10, HeaderVersion(1))); - assert!(!valid_header_version(10, HeaderVersion(2))); - assert!(valid_header_version( - FLOONET_FIRST_HARD_FORK - 1, - HeaderVersion(1) - )); - assert!(valid_header_version( - FLOONET_FIRST_HARD_FORK, - HeaderVersion(2) - )); - assert!(valid_header_version( - FLOONET_FIRST_HARD_FORK + 1, - HeaderVersion(2) - )); - assert!(!valid_header_version( - FLOONET_FIRST_HARD_FORK, - HeaderVersion(1) - )); - assert!(!valid_header_version(YEAR_HEIGHT, HeaderVersion(1))); - assert!(valid_header_version( - FLOONET_SECOND_HARD_FORK - 1, - HeaderVersion(2) - )); - assert!(valid_header_version( - FLOONET_SECOND_HARD_FORK, - HeaderVersion(3) - )); - assert!(valid_header_version( - FLOONET_SECOND_HARD_FORK + 1, - HeaderVersion(3) - )); - assert!(!valid_header_version( - FLOONET_SECOND_HARD_FORK, - HeaderVersion(2) - )); - assert!(!valid_header_version( - FLOONET_SECOND_HARD_FORK, - HeaderVersion(1) - )); - - assert!(!valid_header_version(YEAR_HEIGHT - 1, HeaderVersion(2))); - assert!(valid_header_version(YEAR_HEIGHT - 1, HeaderVersion(3))); - assert!(valid_header_version(YEAR_HEIGHT, HeaderVersion(3))); - assert!(valid_header_version(YEAR_HEIGHT + 1, HeaderVersion(3))); - // v4 not active yet - assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(4))); - assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(3))); - assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(2))); - assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(1))); - assert!(!valid_header_version(YEAR_HEIGHT * 2, HeaderVersion(3))); - assert!(!valid_header_version( - YEAR_HEIGHT * 3 / 2 + 1, - HeaderVersion(3) - )); - } -} diff --git a/core/tests/consensus_automated.rs b/core/tests/consensus_automated.rs new file mode 100644 index 00000000..d1012cd7 --- /dev/null +++ b/core/tests/consensus_automated.rs @@ -0,0 +1,158 @@ +// Copyright 2020 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chrono::Utc; +use grin_core::consensus::{ + next_difficulty, HeaderInfo, AR_SCALE_DAMP_FACTOR, BLOCK_TIME_SEC, DIFFICULTY_ADJUST_WINDOW, + MIN_DIFFICULTY, +}; +use grin_core::global; +use grin_core::pow::Difficulty; + +/// Checks different next_target adjustments and difficulty boundaries +#[test] +fn next_target_adjustment() { + global::set_mining_mode(global::ChainTypes::AutomatedTesting); + let cur_time = Utc::now().timestamp() as u64; + let diff_min = Difficulty::min(); + + // Check we don't get stuck on difficulty <= MIN_DIFFICULTY (at 4x faster blocks at least) + let mut hi = HeaderInfo::from_diff_scaling(diff_min, AR_SCALE_DAMP_FACTOR as u32); + hi.is_secondary = false; + let hinext = next_difficulty( + 1, + repeat( + BLOCK_TIME_SEC / 4, + hi.clone(), + DIFFICULTY_ADJUST_WINDOW, + None, + ), + ); + + assert_ne!(hinext.difficulty, diff_min); + + // Check we don't get stuck on scale MIN_DIFFICULTY, when primary frequency is too high + assert_ne!(hinext.secondary_scaling, MIN_DIFFICULTY as u32); + + // just enough data, right interval, should stay constant + let just_enough = DIFFICULTY_ADJUST_WINDOW + 1; + hi.difficulty = Difficulty::from_num(10000); + assert_eq!( + next_difficulty(1, repeat(BLOCK_TIME_SEC, hi.clone(), just_enough, None)).difficulty, + Difficulty::from_num(10000) + ); + + // check pre difficulty_data_to_vector effect on retargetting + assert_eq!( + next_difficulty(1, vec![HeaderInfo::from_ts_diff(42, hi.difficulty)]).difficulty, + Difficulty::from_num(14913) + ); + + // checking averaging works + hi.difficulty = Difficulty::from_num(500); + let sec = DIFFICULTY_ADJUST_WINDOW / 2; + let mut s1 = repeat(BLOCK_TIME_SEC, hi.clone(), sec, Some(cur_time)); + let mut s2 = repeat_offs( + cur_time + (sec * BLOCK_TIME_SEC) as u64, + BLOCK_TIME_SEC, + 1500, + DIFFICULTY_ADJUST_WINDOW / 2, + ); + s2.append(&mut s1); + assert_eq!( + next_difficulty(1, s2).difficulty, + Difficulty::from_num(1000) + ); + + // too slow, diff goes down + hi.difficulty = Difficulty::from_num(1000); + assert_eq!( + next_difficulty(1, repeat(90, hi.clone(), just_enough, None)).difficulty, + Difficulty::from_num(857) + ); + assert_eq!( + next_difficulty(1, repeat(120, hi.clone(), just_enough, None)).difficulty, + Difficulty::from_num(750) + ); + + // too fast, diff goes up + assert_eq!( + next_difficulty(1, repeat(55, hi.clone(), just_enough, None)).difficulty, + Difficulty::from_num(1028) + ); + assert_eq!( + next_difficulty(1, repeat(45, hi.clone(), just_enough, None)).difficulty, + Difficulty::from_num(1090) + ); + assert_eq!( + next_difficulty(1, repeat(30, hi.clone(), just_enough, None)).difficulty, + Difficulty::from_num(1200) + ); + + // hitting lower time bound, should always get the same result below + assert_eq!( + next_difficulty(1, repeat(0, hi.clone(), just_enough, None)).difficulty, + Difficulty::from_num(1500) + ); + + // hitting higher time bound, should always get the same result above + assert_eq!( + next_difficulty(1, repeat(300, hi.clone(), just_enough, None)).difficulty, + Difficulty::from_num(500) + ); + assert_eq!( + next_difficulty(1, repeat(400, hi.clone(), just_enough, None)).difficulty, + Difficulty::from_num(500) + ); + + // We should never drop below minimum + hi.difficulty = Difficulty::zero(); + assert_eq!( + next_difficulty(1, repeat(90, hi.clone(), just_enough, None)).difficulty, + Difficulty::min() + ); +} + +// Builds an iterator for next difficulty calculation with the provided +// constant time interval, difficulty and total length. +fn repeat(interval: u64, diff: HeaderInfo, len: u64, cur_time: Option) -> Vec { + let cur_time = match cur_time { + Some(t) => t, + None => Utc::now().timestamp() as u64, + }; + // watch overflow here, length shouldn't be ridiculous anyhow + assert!(len < std::usize::MAX as u64); + let diffs = vec![diff.difficulty.clone(); len as usize]; + let times = (0..(len as usize)).map(|n| n * interval as usize).rev(); + let pairs = times.zip(diffs.iter()); + pairs + .map(|(t, d)| { + HeaderInfo::new( + diff.block_hash, + cur_time + t as u64, + d.clone(), + diff.secondary_scaling, + diff.is_secondary, + ) + }) + .collect::>() +} + +fn repeat_offs(from: u64, interval: u64, diff: u64, len: u64) -> Vec { + repeat( + interval, + HeaderInfo::from_ts_diff(1, Difficulty::from_num(diff)), + len, + Some(from), + ) +} diff --git a/core/tests/consensus_floonet.rs b/core/tests/consensus_floonet.rs new file mode 100644 index 00000000..777b52ef --- /dev/null +++ b/core/tests/consensus_floonet.rs @@ -0,0 +1,123 @@ +// Copyright 2020 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use grin_core::consensus::{ + secondary_pow_ratio, valid_header_version, FLOONET_FIRST_HARD_FORK, FLOONET_SECOND_HARD_FORK, + YEAR_HEIGHT, +}; +use grin_core::core::HeaderVersion; +use grin_core::global; + +#[test] +fn test_secondary_pow_ratio() { + // Tests for Floonet chain type (covers pre and post hardfork). + global::set_mining_mode(global::ChainTypes::Floonet); + assert_eq!(global::is_floonet(), true); + + assert_eq!(secondary_pow_ratio(1), 90); + assert_eq!(secondary_pow_ratio(89), 90); + assert_eq!(secondary_pow_ratio(90), 90); + assert_eq!(secondary_pow_ratio(91), 90); + assert_eq!(secondary_pow_ratio(179), 90); + assert_eq!(secondary_pow_ratio(180), 90); + assert_eq!(secondary_pow_ratio(181), 90); + + let one_week = 60 * 24 * 7; + assert_eq!(secondary_pow_ratio(one_week - 1), 90); + assert_eq!(secondary_pow_ratio(one_week), 90); + assert_eq!(secondary_pow_ratio(one_week + 1), 90); + + let two_weeks = one_week * 2; + assert_eq!(secondary_pow_ratio(two_weeks - 1), 89); + assert_eq!(secondary_pow_ratio(two_weeks), 89); + assert_eq!(secondary_pow_ratio(two_weeks + 1), 89); + + let t4_fork_height = 64_000; + assert_eq!(secondary_pow_ratio(t4_fork_height - 1), 85); + assert_eq!(secondary_pow_ratio(t4_fork_height), 85); + assert_eq!(secondary_pow_ratio(t4_fork_height + 1), 85); + + let one_year = one_week * 52; + assert_eq!(secondary_pow_ratio(one_year), 45); + + let ninety_one_weeks = one_week * 91; + assert_eq!(secondary_pow_ratio(ninety_one_weeks - 1), 12); + assert_eq!(secondary_pow_ratio(ninety_one_weeks), 12); + assert_eq!(secondary_pow_ratio(ninety_one_weeks + 1), 12); + + let two_year = one_year * 2; + assert_eq!(secondary_pow_ratio(two_year - 1), 1); + assert_eq!(secondary_pow_ratio(two_year), 0); + assert_eq!(secondary_pow_ratio(two_year + 1), 0); +} + +#[test] +fn hard_forks() { + global::set_mining_mode(global::ChainTypes::Floonet); + assert_eq!(global::is_floonet(), true); + assert!(valid_header_version(0, HeaderVersion(1))); + assert!(valid_header_version(10, HeaderVersion(1))); + assert!(!valid_header_version(10, HeaderVersion(2))); + assert!(valid_header_version( + FLOONET_FIRST_HARD_FORK - 1, + HeaderVersion(1) + )); + assert!(valid_header_version( + FLOONET_FIRST_HARD_FORK, + HeaderVersion(2) + )); + assert!(valid_header_version( + FLOONET_FIRST_HARD_FORK + 1, + HeaderVersion(2) + )); + assert!(!valid_header_version( + FLOONET_FIRST_HARD_FORK, + HeaderVersion(1) + )); + assert!(!valid_header_version(YEAR_HEIGHT, HeaderVersion(1))); + assert!(valid_header_version( + FLOONET_SECOND_HARD_FORK - 1, + HeaderVersion(2) + )); + assert!(valid_header_version( + FLOONET_SECOND_HARD_FORK, + HeaderVersion(3) + )); + assert!(valid_header_version( + FLOONET_SECOND_HARD_FORK + 1, + HeaderVersion(3) + )); + assert!(!valid_header_version( + FLOONET_SECOND_HARD_FORK, + HeaderVersion(2) + )); + assert!(!valid_header_version( + FLOONET_SECOND_HARD_FORK, + HeaderVersion(1) + )); + + assert!(!valid_header_version(YEAR_HEIGHT - 1, HeaderVersion(2))); + assert!(valid_header_version(YEAR_HEIGHT - 1, HeaderVersion(3))); + assert!(valid_header_version(YEAR_HEIGHT, HeaderVersion(3))); + assert!(valid_header_version(YEAR_HEIGHT + 1, HeaderVersion(3))); + // v4 not active yet + assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(4))); + assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(3))); + assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(2))); + assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(1))); + assert!(!valid_header_version(YEAR_HEIGHT * 2, HeaderVersion(3))); + assert!(!valid_header_version( + YEAR_HEIGHT * 3 / 2 + 1, + HeaderVersion(3) + )); +} diff --git a/core/tests/consensus_mainnet.rs b/core/tests/consensus_mainnet.rs new file mode 100644 index 00000000..417dd112 --- /dev/null +++ b/core/tests/consensus_mainnet.rs @@ -0,0 +1,465 @@ +// Copyright 2020 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! core consensus mainnet tests (separated to de-clutter consensus_mainnet) + +//! Setting global::mining_mode() changes global shared state so automated tests should only use one +//! mining mode/chain type per test file to avoid non-deterministic behaviour. + +use grin_core as core; + +use self::core::consensus::*; +use self::core::core::block::HeaderVersion; +use self::core::global; +use self::core::pow::Difficulty; +use chrono::prelude::Utc; +use std::fmt::{self, Display}; + +/// Last n blocks for difficulty calculation purposes +/// (copied from stats in server crate) +#[derive(Clone, Debug)] +pub struct DiffBlock { + /// Block number (can be negative for a new chain) + pub block_number: i64, + /// Block network difficulty + pub difficulty: u64, + /// Time block was found (epoch seconds) + pub time: u64, + /// Duration since previous block (epoch seconds) + pub duration: u64, +} + +/// Stats on the last WINDOW blocks and the difficulty calculation +/// (Copied from stats in server crate) +#[derive(Clone)] +pub struct DiffStats { + /// latest height + pub height: u64, + /// Last WINDOW block data + pub last_blocks: Vec, + /// Average block time for last WINDOW blocks + pub average_block_time: u64, + /// Average WINDOW difficulty + pub average_difficulty: u64, + /// WINDOW size + pub window_size: u64, + /// Block time sum + pub block_time_sum: u64, + /// Block diff sum + pub block_diff_sum: u64, + /// latest ts + pub latest_ts: u64, + /// earliest ts + pub earliest_ts: u64, + /// ts delta + pub ts_delta: u64, +} + +impl Display for DiffBlock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = format!( + "Block Number: {} Difficulty: {}, Time: {}, Duration: {}", + self.block_number, self.difficulty, self.time, self.duration + ); + Display::fmt(&output, f) + } +} + +// Creates a new chain with a genesis at a simulated difficulty +fn create_chain_sim(diff: u64) -> Vec<(HeaderInfo, DiffStats)> { + println!( + "adding create: {}, {}", + Utc::now().timestamp(), + Difficulty::from_num(diff) + ); + let return_vec = vec![HeaderInfo::from_ts_diff( + Utc::now().timestamp() as u64, + Difficulty::from_num(diff), + )]; + let diff_stats = get_diff_stats(&return_vec); + vec![( + HeaderInfo::from_ts_diff(Utc::now().timestamp() as u64, Difficulty::from_num(diff)), + diff_stats, + )] +} + +fn get_diff_stats(chain_sim: &Vec) -> DiffStats { + // Fill out some difficulty stats for convenience + let diff_iter = chain_sim.clone(); + let last_blocks: Vec = global::difficulty_data_to_vector(diff_iter.iter().cloned()); + + let mut last_time = last_blocks[0].timestamp; + let tip_height = chain_sim.len(); + let earliest_block_height = tip_height as i64 - last_blocks.len() as i64; + + let earliest_ts = last_blocks[0].timestamp; + let latest_ts = last_blocks[last_blocks.len() - 1].timestamp; + + let mut i = 1; + + let sum_blocks: Vec = global::difficulty_data_to_vector(diff_iter.iter().cloned()) + .into_iter() + .take(DIFFICULTY_ADJUST_WINDOW as usize) + .collect(); + + let sum_entries: Vec = sum_blocks + .iter() + //.skip(1) + .map(|n| { + let dur = n.timestamp - last_time; + let height = earliest_block_height + i + 1; + i += 1; + last_time = n.timestamp; + DiffBlock { + block_number: height, + difficulty: n.difficulty.to_num(), + time: n.timestamp, + duration: dur, + } + }) + .collect(); + + let block_time_sum = sum_entries.iter().fold(0, |sum, t| sum + t.duration); + let block_diff_sum = sum_entries.iter().fold(0, |sum, d| sum + d.difficulty); + + i = 1; + last_time = last_blocks[0].clone().timestamp; + + let diff_entries: Vec = last_blocks + .iter() + .skip(1) + .map(|n| { + let dur = n.timestamp - last_time; + let height = earliest_block_height + i; + i += 1; + last_time = n.timestamp; + DiffBlock { + block_number: height, + difficulty: n.difficulty.to_num(), + time: n.timestamp, + duration: dur, + } + }) + .collect(); + + DiffStats { + height: tip_height as u64, + last_blocks: diff_entries, + average_block_time: block_time_sum / (DIFFICULTY_ADJUST_WINDOW), + average_difficulty: block_diff_sum / (DIFFICULTY_ADJUST_WINDOW), + window_size: DIFFICULTY_ADJUST_WINDOW, + block_time_sum: block_time_sum, + block_diff_sum: block_diff_sum, + latest_ts: latest_ts, + earliest_ts: earliest_ts, + ts_delta: latest_ts - earliest_ts, + } +} + +// Adds another 'block' to the iterator, so to speak, with difficulty calculated +// from the difficulty adjustment at interval seconds from the previous block +fn add_block( + interval: u64, + chain_sim: Vec<(HeaderInfo, DiffStats)>, +) -> Vec<(HeaderInfo, DiffStats)> { + let mut ret_chain_sim = chain_sim.clone(); + let mut return_chain: Vec = chain_sim.clone().iter().map(|e| e.0.clone()).collect(); + // get last interval + let diff = next_difficulty(1, return_chain.clone()); + let last_elem = chain_sim.first().unwrap().clone().0; + let time = last_elem.timestamp + interval; + return_chain.insert(0, HeaderInfo::from_ts_diff(time, diff.difficulty)); + let diff_stats = get_diff_stats(&return_chain); + ret_chain_sim.insert( + 0, + (HeaderInfo::from_ts_diff(time, diff.difficulty), diff_stats), + ); + ret_chain_sim +} + +// Adds another n 'blocks' to the iterator, with difficulty calculated +fn add_block_repeated( + interval: u64, + chain_sim: Vec<(HeaderInfo, DiffStats)>, + iterations: usize, +) -> Vec<(HeaderInfo, DiffStats)> { + let mut return_chain = chain_sim.clone(); + for _ in 0..iterations { + return_chain = add_block(interval, return_chain.clone()); + } + return_chain +} + +// Prints the contents of the iterator and its difficulties.. useful for +// tweaking +fn print_chain_sim(chain_sim: Vec<(HeaderInfo, DiffStats)>) { + let mut chain_sim = chain_sim.clone(); + chain_sim.reverse(); + let mut last_time = 0; + let mut first = true; + println!("Constants"); + println!("DIFFICULTY_ADJUST_WINDOW: {}", DIFFICULTY_ADJUST_WINDOW); + println!("BLOCK_TIME_WINDOW: {}", BLOCK_TIME_WINDOW); + println!("CLAMP_FACTOR: {}", CLAMP_FACTOR); + println!("DAMP_FACTOR: {}", DIFFICULTY_DAMP_FACTOR); + chain_sim.iter().enumerate().for_each(|(i, b)| { + let block = b.0.clone(); + let stats = b.1.clone(); + if first { + last_time = block.timestamp; + first = false; + } + println!( + "Height: {}, Time: {}, Interval: {}, Network difficulty:{}, Average Block Time: {}, Average Difficulty {}, Block Time Sum: {}, Block Diff Sum: {}, Latest Timestamp: {}, Earliest Timestamp: {}, Timestamp Delta: {}", + i, + block.timestamp, + block.timestamp - last_time, + block.difficulty, + stats.average_block_time, + stats.average_difficulty, + stats.block_time_sum, + stats.block_diff_sum, + stats.latest_ts, + stats.earliest_ts, + stats.ts_delta, + ); + let mut sb = stats.last_blocks.clone(); + sb.reverse(); + for i in sb { + println!(" {}", i); + } + last_time = block.timestamp; + }); +} + +/// Checks different next_target adjustments and difficulty boundaries +#[test] +fn adjustment_scenarios() { + // Use production parameters for genesis diff + global::set_mining_mode(global::ChainTypes::Mainnet); + + // Genesis block with initial diff + let chain_sim = create_chain_sim(global::initial_block_difficulty()); + // Scenario 1) Hash power is massively over estimated, first block takes an hour + let chain_sim = add_block_repeated(3600, chain_sim, 2); + let chain_sim = add_block_repeated(1800, chain_sim, 2); + let chain_sim = add_block_repeated(900, chain_sim, 10); + let chain_sim = add_block_repeated(450, chain_sim, 30); + let chain_sim = add_block_repeated(400, chain_sim, 30); + let chain_sim = add_block_repeated(300, chain_sim, 30); + + println!("*********************************************************"); + println!("Scenario 1) Grossly over-estimated genesis difficulty "); + println!("*********************************************************"); + print_chain_sim(chain_sim); + println!("*********************************************************"); + + // Under-estimated difficulty + let chain_sim = create_chain_sim(global::initial_block_difficulty()); + let chain_sim = add_block_repeated(1, chain_sim, 5); + let chain_sim = add_block_repeated(20, chain_sim, 5); + let chain_sim = add_block_repeated(30, chain_sim, 20); + + println!("*********************************************************"); + println!("Scenario 2) Grossly under-estimated genesis difficulty "); + println!("*********************************************************"); + print_chain_sim(chain_sim); + println!("*********************************************************"); + let just_enough = (DIFFICULTY_ADJUST_WINDOW) as usize; + + // Steady difficulty for a good while, then a sudden drop + let chain_sim = create_chain_sim(global::initial_block_difficulty()); + let chain_sim = add_block_repeated(60, chain_sim, just_enough as usize); + let chain_sim = add_block_repeated(600, chain_sim, 60); + + println!(""); + println!("*********************************************************"); + println!("Scenario 3) Sudden drop in hashpower"); + println!("*********************************************************"); + print_chain_sim(chain_sim); + println!("*********************************************************"); + + // Sudden increase + let chain_sim = create_chain_sim(global::initial_block_difficulty()); + let chain_sim = add_block_repeated(60, chain_sim, just_enough as usize); + let chain_sim = add_block_repeated(10, chain_sim, 10); + + println!(""); + println!("*********************************************************"); + println!("Scenario 4) Sudden increase in hashpower"); + println!("*********************************************************"); + print_chain_sim(chain_sim); + println!("*********************************************************"); + + // Oscillations + let chain_sim = create_chain_sim(global::initial_block_difficulty()); + let chain_sim = add_block_repeated(60, chain_sim, just_enough as usize); + let chain_sim = add_block_repeated(10, chain_sim, 10); + let chain_sim = add_block_repeated(60, chain_sim, 20); + let chain_sim = add_block_repeated(10, chain_sim, 10); + + println!(""); + println!("*********************************************************"); + println!("Scenario 5) Oscillations in hashpower"); + println!("*********************************************************"); + print_chain_sim(chain_sim); + println!("*********************************************************"); +} + +#[test] +fn test_secondary_pow_ratio() { + global::set_mining_mode(global::ChainTypes::Mainnet); + assert_eq!(global::is_floonet(), false); + + assert_eq!(secondary_pow_ratio(1), 90); + assert_eq!(secondary_pow_ratio(89), 90); + assert_eq!(secondary_pow_ratio(90), 90); + assert_eq!(secondary_pow_ratio(91), 90); + assert_eq!(secondary_pow_ratio(179), 90); + assert_eq!(secondary_pow_ratio(180), 90); + assert_eq!(secondary_pow_ratio(181), 90); + + let one_week = 60 * 24 * 7; + assert_eq!(secondary_pow_ratio(one_week - 1), 90); + assert_eq!(secondary_pow_ratio(one_week), 90); + assert_eq!(secondary_pow_ratio(one_week + 1), 90); + + let two_weeks = one_week * 2; + assert_eq!(secondary_pow_ratio(two_weeks - 1), 89); + assert_eq!(secondary_pow_ratio(two_weeks), 89); + assert_eq!(secondary_pow_ratio(two_weeks + 1), 89); + + let t4_fork_height = 64_000; + assert_eq!(secondary_pow_ratio(t4_fork_height - 1), 85); + assert_eq!(secondary_pow_ratio(t4_fork_height), 85); + assert_eq!(secondary_pow_ratio(t4_fork_height + 1), 85); + + let one_year = one_week * 52; + assert_eq!(secondary_pow_ratio(one_year), 45); + + let ninety_one_weeks = one_week * 91; + assert_eq!(secondary_pow_ratio(ninety_one_weeks - 1), 12); + assert_eq!(secondary_pow_ratio(ninety_one_weeks), 12); + assert_eq!(secondary_pow_ratio(ninety_one_weeks + 1), 12); + + let two_year = one_year * 2; + assert_eq!(secondary_pow_ratio(two_year - 1), 1); + assert_eq!(secondary_pow_ratio(two_year), 0); + assert_eq!(secondary_pow_ratio(two_year + 1), 0); +} + +#[test] +fn test_secondary_pow_scale() { + let window = DIFFICULTY_ADJUST_WINDOW; + let mut hi = HeaderInfo::from_diff_scaling(Difficulty::from_num(10), 100); + + global::set_mining_mode(global::ChainTypes::Mainnet); + assert_eq!(global::is_floonet(), false); + + // all primary, factor should increase so it becomes easier to find a high + // difficulty block + hi.is_secondary = false; + assert_eq!( + secondary_pow_scaling(1, &(0..window).map(|_| hi.clone()).collect::>()), + 108 + ); + // all secondary on 90%, factor should go down a bit + hi.is_secondary = true; + assert_eq!( + secondary_pow_scaling(1, &(0..window).map(|_| hi.clone()).collect::>()), + 99 + ); + // all secondary on 1%, factor should go down to bound (divide by 2) + assert_eq!( + secondary_pow_scaling( + 2 * YEAR_HEIGHT * 83 / 90, + &(0..window).map(|_| hi.clone()).collect::>() + ), + 50 + ); + // same as above, testing lowest bound + let mut low_hi = HeaderInfo::from_diff_scaling(Difficulty::from_num(10), MIN_AR_SCALE as u32); + low_hi.is_secondary = true; + assert_eq!( + secondary_pow_scaling( + 2 * YEAR_HEIGHT, + &(0..window).map(|_| low_hi.clone()).collect::>() + ), + MIN_AR_SCALE as u32 + ); + // the right ratio of 95% secondary + let mut primary_hi = HeaderInfo::from_diff_scaling(Difficulty::from_num(10), 50); + primary_hi.is_secondary = false; + assert_eq!( + secondary_pow_scaling( + 1, + &(0..(window / 10)) + .map(|_| primary_hi.clone()) + .chain((0..(window * 9 / 10)).map(|_| hi.clone())) + .collect::>() + ), + 95, // avg ar_scale of 10% * 50 + 90% * 100 + ); + // 95% secondary, should come down based on 97.5 average + assert_eq!( + secondary_pow_scaling( + 1, + &(0..(window / 20)) + .map(|_| primary_hi.clone()) + .chain((0..(window * 95 / 100)).map(|_| hi.clone())) + .collect::>() + ), + 97 + ); + // 40% secondary, should come up based on 70 average + assert_eq!( + secondary_pow_scaling( + 1, + &(0..(window * 6 / 10)) + .map(|_| primary_hi.clone()) + .chain((0..(window * 4 / 10)).map(|_| hi.clone())) + .collect::>() + ), + 73 + ); +} + +#[test] +fn hard_forks() { + global::set_mining_mode(global::ChainTypes::Mainnet); + assert_eq!(global::is_floonet(), false); + assert!(valid_header_version(0, HeaderVersion(1))); + assert!(valid_header_version(10, HeaderVersion(1))); + assert!(!valid_header_version(10, HeaderVersion(2))); + assert!(valid_header_version(YEAR_HEIGHT / 2 - 1, HeaderVersion(1))); + assert!(valid_header_version(YEAR_HEIGHT / 2, HeaderVersion(2))); + assert!(valid_header_version(YEAR_HEIGHT / 2 + 1, HeaderVersion(2))); + assert!(!valid_header_version(YEAR_HEIGHT / 2, HeaderVersion(1))); + assert!(!valid_header_version(YEAR_HEIGHT, HeaderVersion(1))); + + assert!(valid_header_version(YEAR_HEIGHT - 1, HeaderVersion(2))); + assert!(valid_header_version(YEAR_HEIGHT, HeaderVersion(3))); + assert!(valid_header_version(YEAR_HEIGHT + 1, HeaderVersion(3))); + assert!(!valid_header_version(YEAR_HEIGHT, HeaderVersion(2))); + assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(2))); + // v4 not active yet + assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(4))); + assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(3))); + assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(2))); + assert!(!valid_header_version(YEAR_HEIGHT * 3 / 2, HeaderVersion(1))); + assert!(!valid_header_version(YEAR_HEIGHT * 2, HeaderVersion(3))); + assert!(!valid_header_version( + YEAR_HEIGHT * 3 / 2 + 1, + HeaderVersion(3) + )); +}