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 @@ {{ i }} blocks {{ i }} block -
+
Estimate
diff --git a/frontend/src/app/components/difficulty/difficulty.component.ts b/frontend/src/app/components/difficulty/difficulty.component.ts index 5d969bf1e..76a996acc 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.ts +++ b/frontend/src/app/components/difficulty/difficulty.component.ts @@ -11,7 +11,7 @@ interface EpochProgress { newDifficultyHeight: number; colorAdjustments: string; colorPreviousAdjustments: string; - remainingTime: number; + estimatedRetargetDate: number; previousRetarget: number; blocksUntilHalving: number; timeUntilHalving: number; @@ -74,7 +74,7 @@ export class DifficultyComponent implements OnInit { colorAdjustments, colorPreviousAdjustments, newDifficultyHeight: da.nextRetargetHeight, - remainingTime: da.remainingTime, + estimatedRetargetDate: da.estimatedRetargetDate, previousRetarget: da.previousRetarget, blocksUntilHalving, timeUntilHalving, diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index f8f0e23b8..1b02ceceb 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -114,11 +114,14 @@ export const restApiDocsData = [ curl: [], response: `{ progressPercent: 44.397234501112074, - difficultyChange: 0.9845932018381687, - estimatedRetargetDate: 1627762478.9111245, + difficultyChange: 98.45932018381687, + estimatedRetargetDate: 1627762478, remainingBlocks: 1121, - remainingTime: 665977.6261244365, - previousRetarget: -4.807005268478962 + remainingTime: 665977, + previousRetarget: -4.807005268478962, + nextRetargetHeight: 741888, + timeAvg: 302328, + timeOffset: 0 }` }, codeSampleTestnet: { @@ -127,11 +130,14 @@ export const restApiDocsData = [ curl: [], response: `{ progressPercent: 44.397234501112074, - difficultyChange: 0.9845932018381687, - estimatedRetargetDate: 1627762478.9111245, + difficultyChange: 98.45932018381687, + estimatedRetargetDate: 1627762478, remainingBlocks: 1121, - remainingTime: 665977.6261244365, - previousRetarget: -4.807005268478962 + remainingTime: 665977, + previousRetarget: -4.807005268478962, + nextRetargetHeight: 741888, + timeAvg: 302328, + timeOffset: 0 }` }, codeSampleSignet: { @@ -140,11 +146,14 @@ export const restApiDocsData = [ curl: [], response: `{ progressPercent: 44.397234501112074, - difficultyChange: 0.9845932018381687, - estimatedRetargetDate: 1627762478.9111245, + difficultyChange: 98.45932018381687, + estimatedRetargetDate: 1627762478, remainingBlocks: 1121, - remainingTime: 665977.6261244365, - previousRetarget: -4.807005268478962 + remainingTime: 665977, + previousRetarget: -4.807005268478962, + nextRetargetHeight: 741888, + timeAvg: 302328, + timeOffset: 0 }` }, codeSampleLiquid: { @@ -153,11 +162,14 @@ export const restApiDocsData = [ curl: [], response: `{ progressPercent: 44.397234501112074, - difficultyChange: 0.9845932018381687, - estimatedRetargetDate: 1627762478.9111245, + difficultyChange: 98.45932018381687, + estimatedRetargetDate: 1627762478, remainingBlocks: 1121, - remainingTime: 665977.6261244365, - previousRetarget: -4.807005268478962 + remainingTime: 665977, + previousRetarget: -4.807005268478962, + nextRetargetHeight: 741888, + timeAvg: 302328, + timeOffset: 0 }` } }