diff --git a/backend/jest.config.ts b/backend/jest.config.ts
index 5576bfe80..14f932f98 100644
--- a/backend/jest.config.ts
+++ b/backend/jest.config.ts
@@ -7,11 +7,14 @@ const config: Config.InitialOptions = {
automock: false,
collectCoverage: true,
collectCoverageFrom: ["./src/**/**.ts"],
- coverageProvider: "v8",
+ coverageProvider: "babel",
coverageThreshold: {
global: {
lines: 1
}
- }
+ },
+ setupFiles: [
+ "./testSetup.ts",
+ ],
}
export default config;
diff --git a/backend/src/__tests__/api/difficulty-adjustment.test.ts b/backend/src/__tests__/api/difficulty-adjustment.test.ts
new file mode 100644
index 000000000..eb774d445
--- /dev/null
+++ b/backend/src/__tests__/api/difficulty-adjustment.test.ts
@@ -0,0 +1,62 @@
+import { calcDifficultyAdjustment, DifficultyAdjustment } from '../../api/difficulty-adjustment';
+
+describe('Mempool Difficulty Adjustment', () => {
+ test('should calculate Difficulty Adjustments properly', () => {
+ const dt = (dtString) => {
+ return 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], DifficultyAdjustment][];
+
+ 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/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts
index 9453505f4..7314fde6f 100644
--- a/backend/src/__tests__/config.test.ts
+++ b/backend/src/__tests__/config.test.ts
@@ -136,5 +136,4 @@ describe('Mempool Backend Config', () => {
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
});
});
-
});
diff --git a/backend/src/api/difficulty-adjustment.ts b/backend/src/api/difficulty-adjustment.ts
index 1f85fdb80..43c6e463f 100644
--- a/backend/src/api/difficulty-adjustment.ts
+++ b/backend/src/api/difficulty-adjustment.ts
@@ -2,6 +2,84 @@ 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; // Bitcoin mainnet
+ const BLOCK_SECONDS_TARGET = 600; // Bitcoin mainnet
+ const TESTNET_MAX_BLOCK_SECONDS = 1200; // Bitcoin testnet
+
+ 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;
+ let timeAvgSecs = BLOCK_SECONDS_TARGET;
+ // Only calculate the estimate once we have 7.2% of blocks in current epoch
+ if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
+ timeAvgSecs = diffSeconds / blocksInEpoch;
+ difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
+ // Max increase is x4 (+300%)
+ if (difficultyChange > 300) {
+ difficultyChange = 300;
+ }
+ // Max decrease is /4 (-75%)
+ if (difficultyChange < -75) {
+ difficultyChange = -75;
+ }
+ }
+
+ // 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 (timeAvgSecs > TESTNET_MAX_BLOCK_SECONDS) {
+ timeAvgSecs = TESTNET_MAX_BLOCK_SECONDS;
+ }
+
+ const secondsSinceLastBlock = nowSeconds - latestBlockTimestamp;
+ if (secondsSinceLastBlock + timeAvgSecs > TESTNET_MAX_BLOCK_SECONDS) {
+ timeOffset = -Math.min(secondsSinceLastBlock, TESTNET_MAX_BLOCK_SECONDS) * 1000;
+ }
+ }
+
+ const timeAvg = Math.floor(timeAvgSecs * 1000);
+ const remainingTime = remainingBlocks * timeAvg;
+ const estimatedRetargetDate = remainingTime + nowSeconds * 1000;
+
+ return {
+ progressPercent,
+ difficultyChange,
+ estimatedRetargetDate,
+ remainingBlocks,
+ remainingTime,
+ previousRetarget,
+ nextRetargetHeight,
+ timeAvg,
+ timeOffset,
+ };
+}
+
class DifficultyAdjustmentApi {
constructor() { }
@@ -11,56 +89,12 @@ class DifficultyAdjustmentApi {
const blockHeight = blocks.getCurrentBlockHeight();
const blocksCache = blocks.getBlocks();
const latestBlock = blocksCache[blocksCache.length - 1];
+ const nowSeconds = Math.floor(new Date().getTime() / 1000);
- const now = new Date().getTime() / 1000;
- const diff = now - DATime;
- const blocksInEpoch = blockHeight % 2016;
- const progressPercent = (blocksInEpoch >= 0) ? blocksInEpoch / 2016 * 100 : 100;
- const remainingBlocks = 2016 - blocksInEpoch;
- const nextRetargetHeight = blockHeight + remainingBlocks;
-
- let difficultyChange = 0;
- if (remainingBlocks < 1870) {
- if (blocksInEpoch > 0) {
- difficultyChange = (600 / (diff / blocksInEpoch) - 1) * 100;
- }
- if (difficultyChange > 300) {
- difficultyChange = 300;
- }
- if (difficultyChange < -75) {
- difficultyChange = -75;
- }
- }
-
- let timeAvgMins = blocksInEpoch && blocksInEpoch > 146 ? diff / 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 (now - latestBlock.timestamp + timeAvgMins * 60 > 1200) {
- timeOffset = -Math.min(now - latestBlock.timestamp, 1200) * 1000;
- }
- }
-
- const timeAvg = timeAvgMins * 60 * 1000 ;
- const remainingTime = (remainingBlocks * timeAvg) + (now * 1000);
- const estimatedRetargetDate = remainingTime + now;
-
- return {
- progressPercent,
- difficultyChange,
- estimatedRetargetDate,
- remainingBlocks,
- remainingTime,
- previousRetarget,
- nextRetargetHeight,
- timeAvg,
- timeOffset,
- };
+ return calcDifficultyAdjustment(
+ DATime, nowSeconds, blockHeight, previousRetarget,
+ config.MEMPOOL.NETWORK, latestBlock.timestamp
+ );
}
}
diff --git a/backend/testSetup.ts b/backend/testSetup.ts
new file mode 100644
index 000000000..ca51bbbe6
--- /dev/null
+++ b/backend/testSetup.ts
@@ -0,0 +1,5 @@
+jest.mock('./mempool-config.json', () => ({}), { virtual: true });
+jest.mock('./src/logger.ts', () => ({}), { virtual: true });
+jest.mock('./src/api/rbf-cache.ts', () => ({}), { virtual: true });
+jest.mock('./src/api/mempool.ts', () => ({}), { virtual: true });
+jest.mock('./src/api/memory-cache.ts', () => ({}), { virtual: true });
diff --git a/frontend/src/app/components/difficulty/difficulty.component.html b/frontend/src/app/components/difficulty/difficulty.component.html
index 3684b8de4..e030f74fa 100644
--- a/frontend/src/app/components/difficulty/difficulty.component.html
+++ b/frontend/src/app/components/difficulty/difficulty.component.html
@@ -10,7 +10,7 @@