Skip to content

Commit

Permalink
Adjust block difficulty calculation
Browse files Browse the repository at this point in the history
Reference: sumoprojects/sumokoin@874c11b
thanks to @haruto-tanno for reference implementation.

Difficulty algorithm adjustment:
- Difficulty window N=17·
- Select M=14 last timespan for median
- Adjust average hashrate to (0.8A + 0.3M)
- Add sorted and cut old timestamps
- Bring median nearer to average value
- Adjust block timespan limit + median block limit to (1440 + 12 block) as suggestion from @zawy12

Narrow gap for timewarp attack:
- Set "block future limit" to 30 minutes (vs old 2 hours)
- Set "block timestamp check window" to 15 block (vs old 60 block)
- Implement a difficult cut for top 6 blocks to neutralize time-warp attacks
  • Loading branch information
thaerkh committed Oct 5, 2017
1 parent 642a0b2 commit 098c22f
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 18 deletions.
78 changes: 78 additions & 0 deletions src/cryptonote_basic/difficulty.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@
#include <cstdint>
#include <vector>

#include "include_base_utils.h"
#include "common/int-util.h"
#include "crypto/hash.h"
#include "cryptonote_config.h"
#include "misc_language.h"
#include "difficulty.h"

#undef MASARI_DEFAULT_LOG_CATEGORY
#define MASARI_DEFAULT_LOG_CATEGORY "difficulty"
#define MAX_AVERAGE_TIMESPAN (uint64_t) DIFFICULTY_TARGET*6 // 24 minutes
#define MIN_AVERAGE_TIMESPAN (uint64_t) DIFFICULTY_TARGET/24 // 10s

namespace cryptonote {

Expand Down Expand Up @@ -162,4 +166,78 @@ namespace cryptonote {
return (low + time_span - 1) / time_span;
}

difficulty_type next_difficulty_v2(std::vector<std::uint64_t> timestamps, std::vector<difficulty_type> cumulative_difficulties, size_t target_seconds) {

if (timestamps.size() > DIFFICULTY_BLOCKS_COUNT_V2)
{
timestamps.resize(DIFFICULTY_BLOCKS_COUNT_V2);
cumulative_difficulties.resize(DIFFICULTY_BLOCKS_COUNT_V2);
}

size_t length = timestamps.size();
assert(length == cumulative_difficulties.size());
if (length <= 1) {
return 1;
}

sort(timestamps.begin(), timestamps.end());
size_t cut_begin, cut_end;
static_assert(2 * DIFFICULTY_CUT_V2 <= DIFFICULTY_BLOCKS_COUNT_V2 - 2, "Cut length is too large");
if (length <= DIFFICULTY_BLOCKS_COUNT_V2 - 2 * DIFFICULTY_CUT_V2) {
cut_begin = 0;
cut_end = length;
}
else {
cut_begin = (length - (DIFFICULTY_BLOCKS_COUNT_V2 - 2 * DIFFICULTY_CUT_V2) + 1) / 2;
cut_end = cut_begin + (DIFFICULTY_BLOCKS_COUNT_V2 - 2 * DIFFICULTY_CUT_V2);
}
assert(/*cut_begin >= 0 &&*/ cut_begin + 2 <= cut_end && cut_end <= length);
uint64_t total_timespan = timestamps[cut_end - 1] - timestamps[cut_begin];
if (total_timespan == 0) {
total_timespan = 1;
}

uint64_t timespan_median = 0;
if (cut_begin > 0 && length >= cut_begin * 2 + 3){
std::vector<std::uint64_t> time_spans;
for (size_t i = length - cut_begin * 2 - 3; i < length - 1; i++){
uint64_t time_span = timestamps[i + 1] - timestamps[i];
if (time_span == 0) {
time_span = 1;
}
time_spans.push_back(time_span);

LOG_PRINT_L3("Timespan " << i << ": " << (time_span / 60) / 60 << ":" << (time_span > 3600 ? (time_span % 3600) / 60 : time_span / 60) << ":" << time_span % 60 << " (" << time_span << ")");
}
timespan_median = epee::misc_utils::median(time_spans);
}

uint64_t timespan_length = length - cut_begin * 2 - 1;
LOG_PRINT_L2("Timespan Median: " << timespan_median << ", Timespan Average: " << total_timespan / timespan_length);

uint64_t total_timespan_median = timespan_median > 0 ? timespan_median * timespan_length : total_timespan * 7 / 10;
uint64_t adjusted_total_timespan = (total_timespan * 8 + total_timespan_median * 3) / 10; // 0.8A + 0.3M (the median of a poisson distribution is 70% of the mean, so 0.25A = 0.25/0.7 = 0.285M)
if (adjusted_total_timespan > MAX_AVERAGE_TIMESPAN * timespan_length){
adjusted_total_timespan = MAX_AVERAGE_TIMESPAN * timespan_length;
}
if (adjusted_total_timespan < MIN_AVERAGE_TIMESPAN * timespan_length){
adjusted_total_timespan = MIN_AVERAGE_TIMESPAN * timespan_length;
}

difficulty_type total_work = cumulative_difficulties[cut_end - 1] - cumulative_difficulties[cut_begin];
assert(total_work > 0);

uint64_t low, high;
mul(total_work, target_seconds, low, high);
if (high != 0) {
return 0;
}

uint64_t next_diff = (low + adjusted_total_timespan - 1) / adjusted_total_timespan;
if (next_diff < 1) next_diff = 1;
LOG_PRINT_L2("Total timespan: " << total_timespan << ", Adjusted total timespan: " << adjusted_total_timespan << ", Total work: " << total_work << ", Next diff: " << next_diff << ", Hashrate (H/s): " << next_diff / target_seconds);

return next_diff;
}

}
2 changes: 2 additions & 0 deletions src/cryptonote_basic/difficulty.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,7 @@ namespace cryptonote
* @return true if valid, else false
*/
bool check_hash(const crypto::hash &hash, difficulty_type difficulty);

difficulty_type next_difficulty_v2(std::vector<std::uint64_t> timestamps, std::vector<difficulty_type> cumulative_difficulties, size_t target_seconds);
difficulty_type next_difficulty(std::vector<std::uint64_t> timestamps, std::vector<difficulty_type> cumulative_difficulties, size_t target_seconds);
}
8 changes: 8 additions & 0 deletions src/cryptonote_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@

#define BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW 60

#define CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT_V2 60*24
#define BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW_V2 12


// MONEY_SUPPLY - total number coins to be generated
#define MONEY_SUPPLY ((uint64_t)(-1))
#define EMISSION_SPEED_FACTOR_PER_MINUTE (20)
Expand All @@ -72,6 +76,10 @@
#define DIFFICULTY_CUT 60 // timestamps to cut after sorting
#define DIFFICULTY_BLOCKS_COUNT DIFFICULTY_WINDOW + DIFFICULTY_LAG

#define DIFFICULTY_WINDOW_V2 17
#define DIFFICULTY_CUT_V2 6
#define DIFFICULTY_BLOCKS_COUNT_V2 DIFFICULTY_WINDOW_V2 + DIFFICULTY_CUT_V2*2

#define CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 DIFFICULTY_TARGET * CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS
#define CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS 1

Expand Down
45 changes: 27 additions & 18 deletions src/cryptonote_core/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ static const struct {
time_t time;
} mainnet_hard_forks[] = {
{ 1, 1, 0, 1504387246 },
{ 2, 28000, 0, 1507601066 }
};

static const struct {
Expand All @@ -97,6 +98,7 @@ static const struct {
time_t time;
} testnet_hard_forks[] = {
{ 1, 1, 0, 1504374656 },
{ 2, 21700, 0, 1507182919 }
};

//------------------------------------------------------------------
Expand Down Expand Up @@ -673,6 +675,8 @@ difficulty_type Blockchain::get_difficulty_for_next_block()
std::vector<uint64_t> timestamps;
std::vector<difficulty_type> difficulties;
auto height = m_db->height();
size_t difficulty_blocks_count = get_current_hard_fork_version() < 2 ? DIFFICULTY_BLOCKS_COUNT : DIFFICULTY_BLOCKS_COUNT_V2;

// ND: Speedup
// 1. Keep a list of the last 735 (or less) blocks that is used to compute difficulty,
// then when the next block difficulty is queried, push the latest height data and
Expand All @@ -684,9 +688,9 @@ difficulty_type Blockchain::get_difficulty_for_next_block()
m_timestamps.push_back(m_db->get_block_timestamp(index));
m_difficulties.push_back(m_db->get_block_cumulative_difficulty(index));

while (m_timestamps.size() > DIFFICULTY_BLOCKS_COUNT)
while (m_timestamps.size() > difficulty_blocks_count)
m_timestamps.erase(m_timestamps.begin());
while (m_difficulties.size() > DIFFICULTY_BLOCKS_COUNT)
while (m_difficulties.size() > difficulty_blocks_count)
m_difficulties.erase(m_difficulties.begin());

m_timestamps_and_difficulties_height = height;
Expand All @@ -695,7 +699,7 @@ difficulty_type Blockchain::get_difficulty_for_next_block()
}
else
{
size_t offset = height - std::min < size_t > (height, static_cast<size_t>(DIFFICULTY_BLOCKS_COUNT));
size_t offset = height - std::min < size_t > (height, static_cast<size_t>(difficulty_blocks_count));
if (offset == 0)
++offset;

Expand All @@ -712,7 +716,7 @@ difficulty_type Blockchain::get_difficulty_for_next_block()
m_difficulties = difficulties;
}
size_t target = DIFFICULTY_TARGET;
return next_difficulty(timestamps, difficulties, target);
return get_current_hard_fork_version() < 2 ? next_difficulty(timestamps, difficulties, target) : next_difficulty_v2(timestamps, difficulties, target);
}
//------------------------------------------------------------------
// This function removes blocks from the blockchain until it gets to the
Expand Down Expand Up @@ -860,16 +864,17 @@ difficulty_type Blockchain::get_next_difficulty_for_alternative_chain(const std:
LOG_PRINT_L3("Blockchain::" << __func__);
std::vector<uint64_t> timestamps;
std::vector<difficulty_type> cumulative_difficulties;
size_t difficulty_blocks_count = get_current_hard_fork_version() < 2 ? DIFFICULTY_BLOCKS_COUNT : DIFFICULTY_BLOCKS_COUNT_V2;

// if the alt chain isn't long enough to calculate the difficulty target
// based on its blocks alone, need to get more blocks from the main chain
if(alt_chain.size()< DIFFICULTY_BLOCKS_COUNT)
if(alt_chain.size()< difficulty_blocks_count)
{
CRITICAL_REGION_LOCAL(m_blockchain_lock);

// Figure out start and stop offsets for main chain blocks
size_t main_chain_stop_offset = alt_chain.size() ? alt_chain.front()->second.height : bei.height;
size_t main_chain_count = DIFFICULTY_BLOCKS_COUNT - std::min(static_cast<size_t>(DIFFICULTY_BLOCKS_COUNT), alt_chain.size());
size_t main_chain_count = difficulty_blocks_count - std::min(static_cast<size_t>(difficulty_blocks_count), alt_chain.size());
main_chain_count = std::min(main_chain_count, main_chain_stop_offset);
size_t main_chain_start_offset = main_chain_stop_offset - main_chain_count;

Expand All @@ -884,7 +889,7 @@ difficulty_type Blockchain::get_next_difficulty_for_alternative_chain(const std:
}

// make sure we haven't accidentally grabbed too many blocks...maybe don't need this check?
CHECK_AND_ASSERT_MES((alt_chain.size() + timestamps.size()) <= DIFFICULTY_BLOCKS_COUNT, false, "Internal error, alt_chain.size()[" << alt_chain.size() << "] + vtimestampsec.size()[" << timestamps.size() << "] NOT <= DIFFICULTY_WINDOW[]" << DIFFICULTY_BLOCKS_COUNT);
CHECK_AND_ASSERT_MES((alt_chain.size() + timestamps.size()) <= difficulty_blocks_count, false, "Internal error, alt_chain.size()[" << alt_chain.size() << "] + vtimestampsec.size()[" << timestamps.size() << "] NOT <= DIFFICULTY_WINDOW[]" << difficulty_blocks_count);

for (auto it : alt_chain)
{
Expand All @@ -896,8 +901,8 @@ difficulty_type Blockchain::get_next_difficulty_for_alternative_chain(const std:
// and timestamps from it alone
else
{
timestamps.resize(static_cast<size_t>(DIFFICULTY_BLOCKS_COUNT));
cumulative_difficulties.resize(static_cast<size_t>(DIFFICULTY_BLOCKS_COUNT));
timestamps.resize(static_cast<size_t>(difficulty_blocks_count));
cumulative_difficulties.resize(static_cast<size_t>(difficulty_blocks_count));
size_t count = 0;
size_t max_i = timestamps.size()-1;
// get difficulties and timestamps from most recent blocks in alt chain
Expand All @@ -906,7 +911,7 @@ difficulty_type Blockchain::get_next_difficulty_for_alternative_chain(const std:
timestamps[max_i - count] = it->second.bl.timestamp;
cumulative_difficulties[max_i - count] = it->second.cumulative_difficulty;
count++;
if(count >= DIFFICULTY_BLOCKS_COUNT)
if(count >= difficulty_blocks_count)
break;
}
}
Expand All @@ -915,7 +920,7 @@ difficulty_type Blockchain::get_next_difficulty_for_alternative_chain(const std:
size_t target = DIFFICULTY_TARGET;

// calculate the difficulty target for the block and return it
return next_difficulty(timestamps, cumulative_difficulties, target);
return get_current_hard_fork_version() < 2 ? next_difficulty(timestamps, cumulative_difficulties, target) : next_difficulty_v2(timestamps, cumulative_difficulties, target);
}
//------------------------------------------------------------------
// This function does a sanity check on basic things that all miner
Expand Down Expand Up @@ -1155,11 +1160,12 @@ bool Blockchain::complete_timestamps_vector(uint64_t start_top_height, std::vect
{
LOG_PRINT_L3("Blockchain::" << __func__);

if(timestamps.size() >= BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW)
size_t blockchain_timestamp_check_window = get_current_hard_fork_version() < 2 ? BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW : BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW_V2;
if(timestamps.size() >= blockchain_timestamp_check_window)
return true;

CRITICAL_REGION_LOCAL(m_blockchain_lock);
size_t need_elements = BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW - timestamps.size();
size_t need_elements = blockchain_timestamp_check_window - timestamps.size();
CHECK_AND_ASSERT_MES(start_top_height < m_db->height(), false, "internal error: passed start_height not < " << " m_db->height() -- " << start_top_height << " >= " << m_db->height());
size_t stop_offset = start_top_height > need_elements ? start_top_height - need_elements : 0;
while (start_top_height != stop_offset)
Expand Down Expand Up @@ -2737,10 +2743,11 @@ bool Blockchain::check_block_timestamp(std::vector<uint64_t>& timestamps, const
{
LOG_PRINT_L3("Blockchain::" << __func__);
uint64_t median_ts = epee::misc_utils::median(timestamps);
size_t blockchain_timestamp_check_window = get_current_hard_fork_version() < 2 ? BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW : BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW_V2;

if(b.timestamp < median_ts)
{
MERROR_VER("Timestamp of block with id: " << get_block_hash(b) << ", " << b.timestamp << ", less than median of last " << BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW << " blocks, " << median_ts);
MERROR_VER("Timestamp of block with id: " << get_block_hash(b) << ", " << b.timestamp << ", less than median of last " << blockchain_timestamp_check_window << " blocks, " << median_ts);
return false;
}

Expand All @@ -2757,14 +2764,16 @@ bool Blockchain::check_block_timestamp(std::vector<uint64_t>& timestamps, const
bool Blockchain::check_block_timestamp(const block& b) const
{
LOG_PRINT_L3("Blockchain::" << __func__);
if(b.timestamp > get_adjusted_time() + CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT)
uint64_t cryptonote_block_future_time_limit = get_current_hard_fork_version() < 2 ? CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT : CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT_V2;
size_t blockchain_timestamp_check_window = get_current_hard_fork_version() < 2 ? BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW : BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW_V2;
if(b.timestamp > get_adjusted_time() + cryptonote_block_future_time_limit)
{
MERROR_VER("Timestamp of block with id: " << get_block_hash(b) << ", " << b.timestamp << ", bigger than adjusted time + 2 hours");
MERROR_VER("Timestamp of block with id: " << get_block_hash(b) << ", " << b.timestamp << ", bigger than adjusted time + " << (get_current_hard_fork_version() < 2 ? "2 hours" : "30 minutes"));
return false;
}

// if not enough blocks, no proper median yet, return true
if(m_db->height() < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW)
if(m_db->height() < blockchain_timestamp_check_window)
{
return true;
}
Expand All @@ -2773,7 +2782,7 @@ bool Blockchain::check_block_timestamp(const block& b) const
auto h = m_db->height();

// need most recent 60 blocks, get index of first of those
size_t offset = h - BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW;
size_t offset = h - blockchain_timestamp_check_window;
for(;offset < h; ++offset)
{
timestamps.push_back(m_db->get_block_timestamp(offset));
Expand Down

0 comments on commit 098c22f

Please sign in to comment.