From a00eb2736b6a52c548691d03f66a55712fd254c8 Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 19 Aug 2022 23:48:59 +0900 Subject: [PATCH] Refactor Difficulty Adjustment calc + unit test it --- backend/src/__tests__/config.test.ts | 58 ++++++++++ backend/src/api/difficulty-adjustment.ts | 131 ++++++++++++++--------- 2 files changed, 137 insertions(+), 52 deletions(-) diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts index 9453505f4..b469d3e07 100644 --- a/backend/src/__tests__/config.test.ts +++ b/backend/src/__tests__/config.test.ts @@ -137,4 +137,62 @@ describe('Mempool Backend Config', () => { }); }); + test('should calculate Difficulty Adjustments properly', () => { + jest.isolateModules(() => { + const { calcDifficultyAdjustment } = jest.requireActual('../api/difficulty-adjustment'); + const dt = dtString => Math.floor(new Date(dtString).getTime() / 1000); + const vectors = [ + [ // Vector 1 + [ // Inputs + dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds) + dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds) + 750134, // Current block height + 0.6280047707459726, // Previous retarget % (Passed through) + 'mainnet', // Network (if testnet, next value is non-zero) + 0, // If not testnet, not used + ], + { // Expected Result + progressPercent: 9.027777777777777, + difficultyChange: 12.562233927411782, + estimatedRetargetDate: 1661895424692, + remainingBlocks: 1834, + remainingTime: 977591692, + previousRetarget: 0.6280047707459726, + nextRetargetHeight: 751968, + timeAvg: 533038, + timeOffset: 0, + }, + ], + [ // Vector 2 (testnet) + [ // Inputs + dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds) + dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds) + 750134, // Current block height + 0.6280047707459726, // Previous retarget % (Passed through) + 'testnet', // Network + dt('2022-08-19T13:52:46.000Z'), // Latest block timestamp in seconds + ], + { // Expected Result is same other than timeOffset + progressPercent: 9.027777777777777, + difficultyChange: 12.562233927411782, + estimatedRetargetDate: 1661895424692, + remainingBlocks: 1834, + remainingTime: 977591692, + previousRetarget: 0.6280047707459726, + nextRetargetHeight: 751968, + timeAvg: 533038, + timeOffset: -667000, // 11 min 7 seconds since last block (testnet only) + // If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes + }, + ], + ] as [[number, number, number, number, string, number], any][]; + + for (const vector of vectors) { + const result = calcDifficultyAdjustment(...vector[0]); + // previousRetarget is passed through untouched + expect(result.previousRetarget).toStrictEqual(vector[0][3]); + expect(result).toStrictEqual(vector[1]); + } + }); + }); }); diff --git a/backend/src/api/difficulty-adjustment.ts b/backend/src/api/difficulty-adjustment.ts index 37a0e7a8d..31d525342 100644 --- a/backend/src/api/difficulty-adjustment.ts +++ b/backend/src/api/difficulty-adjustment.ts @@ -2,69 +2,96 @@ import config from '../config'; import { IDifficultyAdjustment } from '../mempool.interfaces'; import blocks from './blocks'; +export interface DifficultyAdjustment { + progressPercent: number; // Percent: 0 to 100 + difficultyChange: number; // Percent: -75 to 300 + estimatedRetargetDate: number; // Unix time in ms + remainingBlocks: number; // Block count + remainingTime: number; // Duration of time in ms + previousRetarget: number; // Percent: -75 to 300 + nextRetargetHeight: number; // Block Height + timeAvg: number; // Duration of time in ms + timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms +} + +export function calcDifficultyAdjustment( + DATime: number, + nowSeconds: number, + blockHeight: number, + previousRetarget: number, + network: string, + latestBlockTimestamp: number, +): DifficultyAdjustment { + const ESTIMATE_LAG_BLOCKS = 146; // For first 7.2% of epoch, don't estimate. + const EPOCH_BLOCK_LENGTH = 2016; + + const diffSeconds = nowSeconds - DATime; + const blocksInEpoch = (blockHeight >= 0) ? blockHeight % EPOCH_BLOCK_LENGTH : 0; + const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100; + const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch; + const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0; + + let difficultyChange = 0; + // Only calculate the estimate once we have 7.2% of blocks in current epoch + if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) { + difficultyChange = (600 / (diffSeconds / blocksInEpoch) - 1) * 100; + // Max increase is x4 (+300%) + if (difficultyChange > 300) { + difficultyChange = 300; + } + // Max decrease is /4 (-75%) + if (difficultyChange < -75) { + difficultyChange = -75; + } + } + + let timeAvgMins = blocksInEpoch >= ESTIMATE_LAG_BLOCKS ? diffSeconds / blocksInEpoch / 60 : 10; + + // Testnet difficulty is set to 1 after 20 minutes of no blocks, + // therefore the time between blocks will always be below 20 minutes (1200s). + let timeOffset = 0; + if (network === 'testnet') { + if (timeAvgMins > 20) { + timeAvgMins = 20; + } + + if (nowSeconds - latestBlockTimestamp + timeAvgMins * 60 > 1200) { + timeOffset = -Math.min(nowSeconds - latestBlockTimestamp, 1200) * 1000; + } + } + + const timeAvg = Math.floor(timeAvgMins * 60 * 1000); + const remainingTime = remainingBlocks * timeAvg; + const estimatedRetargetDate = remainingTime + nowSeconds * 1000; + + return { + progressPercent, + difficultyChange, + estimatedRetargetDate, + remainingBlocks, + remainingTime, + previousRetarget, + nextRetargetHeight, + timeAvg, + timeOffset, + }; +} + class DifficultyAdjustmentApi { constructor() { } public getDifficultyAdjustment(): IDifficultyAdjustment { - const ESTIMATE_LAG_BLOCKS = 146; // For first 7.2% of epoch, don't estimate. - const EPOCH_BLOCK_LENGTH = 2016; - const DATime = blocks.getLastDifficultyAdjustmentTime(); const previousRetarget = blocks.getPreviousDifficultyRetarget(); const blockHeight = blocks.getCurrentBlockHeight(); const blocksCache = blocks.getBlocks(); const latestBlock = blocksCache[blocksCache.length - 1]; - const nowSeconds = Math.floor(new Date().getTime() / 1000); - const diffSeconds = nowSeconds - DATime; - const blocksInEpoch = (blockHeight >= 0) ? blockHeight % EPOCH_BLOCK_LENGTH : 0; - const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100; - const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch; - const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0; - let difficultyChange = 0; - // Only calculate the estimate once we have 7.2% of blocks in current epoch - if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) { - difficultyChange = (600 / (diffSeconds / blocksInEpoch) - 1) * 100; - // Max increase is x4 (+300%) - if (difficultyChange > 300) { - difficultyChange = 300; - } - // Max decrease is /4 (-75%) - if (difficultyChange < -75) { - difficultyChange = -75; - } - } - - let timeAvgMins = blocksInEpoch >= ESTIMATE_LAG_BLOCKS ? diffSeconds / blocksInEpoch / 60 : 10; - - // Testnet difficulty is set to 1 after 20 minutes of no blocks, - // therefore the time between blocks will always be below 20 minutes (1200s). - let timeOffset = 0; - if (config.MEMPOOL.NETWORK === 'testnet') { - if (timeAvgMins > 20) { - timeAvgMins = 20; - } - if (nowSeconds - latestBlock.timestamp + timeAvgMins * 60 > 1200) { - timeOffset = -Math.min(nowSeconds - latestBlock.timestamp, 1200) * 1000; - } - } - - const timeAvg = Math.floor(timeAvgMins * 60 * 1000); - const remainingTime = remainingBlocks * timeAvg; - const estimatedRetargetDate = remainingTime + nowSeconds * 1000; - - return { - progressPercent, - difficultyChange, - estimatedRetargetDate, - remainingBlocks, - remainingTime, - previousRetarget, - nextRetargetHeight, - timeAvg, - timeOffset, - }; + return calcDifficultyAdjustment( + DATime, nowSeconds, blockHeight, previousRetarget, + config.MEMPOOL.NETWORK, latestBlock.timestamp + ); } }