Merge pull request #2333 from mempool/fix/difficulty-api
Fix: Difficulty API (REST) with frontend fixes
This commit is contained in:
commit
69d4ba18d5
@ -7,11 +7,14 @@ const config: Config.InitialOptions = {
|
|||||||
automock: false,
|
automock: false,
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
collectCoverageFrom: ["./src/**/**.ts"],
|
collectCoverageFrom: ["./src/**/**.ts"],
|
||||||
coverageProvider: "v8",
|
coverageProvider: "babel",
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
lines: 1
|
lines: 1
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
setupFiles: [
|
||||||
|
"./testSetup.ts",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
export default config;
|
export default config;
|
||||||
|
62
backend/src/__tests__/api/difficulty-adjustment.test.ts
Normal file
62
backend/src/__tests__/api/difficulty-adjustment.test.ts
Normal file
@ -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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -136,5 +136,4 @@ describe('Mempool Backend Config', () => {
|
|||||||
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,84 @@ import config from '../config';
|
|||||||
import { IDifficultyAdjustment } from '../mempool.interfaces';
|
import { IDifficultyAdjustment } from '../mempool.interfaces';
|
||||||
import blocks from './blocks';
|
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 {
|
class DifficultyAdjustmentApi {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@ -11,56 +89,12 @@ class DifficultyAdjustmentApi {
|
|||||||
const blockHeight = blocks.getCurrentBlockHeight();
|
const blockHeight = blocks.getCurrentBlockHeight();
|
||||||
const blocksCache = blocks.getBlocks();
|
const blocksCache = blocks.getBlocks();
|
||||||
const latestBlock = blocksCache[blocksCache.length - 1];
|
const latestBlock = blocksCache[blocksCache.length - 1];
|
||||||
|
const nowSeconds = Math.floor(new Date().getTime() / 1000);
|
||||||
|
|
||||||
const now = new Date().getTime() / 1000;
|
return calcDifficultyAdjustment(
|
||||||
const diff = now - DATime;
|
DATime, nowSeconds, blockHeight, previousRetarget,
|
||||||
const blocksInEpoch = blockHeight % 2016;
|
config.MEMPOOL.NETWORK, latestBlock.timestamp
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
backend/testSetup.ts
Normal file
5
backend/testSetup.ts
Normal file
@ -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 });
|
@ -10,7 +10,7 @@
|
|||||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div class="symbol"><app-time-until [time]="epochData.remainingTime" [fastRender]="true"></app-time-until></div>
|
<div class="symbol"><app-time-until [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time-until></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||||
|
@ -11,7 +11,7 @@ interface EpochProgress {
|
|||||||
newDifficultyHeight: number;
|
newDifficultyHeight: number;
|
||||||
colorAdjustments: string;
|
colorAdjustments: string;
|
||||||
colorPreviousAdjustments: string;
|
colorPreviousAdjustments: string;
|
||||||
remainingTime: number;
|
estimatedRetargetDate: number;
|
||||||
previousRetarget: number;
|
previousRetarget: number;
|
||||||
blocksUntilHalving: number;
|
blocksUntilHalving: number;
|
||||||
timeUntilHalving: number;
|
timeUntilHalving: number;
|
||||||
@ -74,7 +74,7 @@ export class DifficultyComponent implements OnInit {
|
|||||||
colorAdjustments,
|
colorAdjustments,
|
||||||
colorPreviousAdjustments,
|
colorPreviousAdjustments,
|
||||||
newDifficultyHeight: da.nextRetargetHeight,
|
newDifficultyHeight: da.nextRetargetHeight,
|
||||||
remainingTime: da.remainingTime,
|
estimatedRetargetDate: da.estimatedRetargetDate,
|
||||||
previousRetarget: da.previousRetarget,
|
previousRetarget: da.previousRetarget,
|
||||||
blocksUntilHalving,
|
blocksUntilHalving,
|
||||||
timeUntilHalving,
|
timeUntilHalving,
|
||||||
|
@ -114,11 +114,14 @@ export const restApiDocsData = [
|
|||||||
curl: [],
|
curl: [],
|
||||||
response: `{
|
response: `{
|
||||||
progressPercent: 44.397234501112074,
|
progressPercent: 44.397234501112074,
|
||||||
difficultyChange: 0.9845932018381687,
|
difficultyChange: 98.45932018381687,
|
||||||
estimatedRetargetDate: 1627762478.9111245,
|
estimatedRetargetDate: 1627762478,
|
||||||
remainingBlocks: 1121,
|
remainingBlocks: 1121,
|
||||||
remainingTime: 665977.6261244365,
|
remainingTime: 665977,
|
||||||
previousRetarget: -4.807005268478962
|
previousRetarget: -4.807005268478962,
|
||||||
|
nextRetargetHeight: 741888,
|
||||||
|
timeAvg: 302328,
|
||||||
|
timeOffset: 0
|
||||||
}`
|
}`
|
||||||
},
|
},
|
||||||
codeSampleTestnet: {
|
codeSampleTestnet: {
|
||||||
@ -127,11 +130,14 @@ export const restApiDocsData = [
|
|||||||
curl: [],
|
curl: [],
|
||||||
response: `{
|
response: `{
|
||||||
progressPercent: 44.397234501112074,
|
progressPercent: 44.397234501112074,
|
||||||
difficultyChange: 0.9845932018381687,
|
difficultyChange: 98.45932018381687,
|
||||||
estimatedRetargetDate: 1627762478.9111245,
|
estimatedRetargetDate: 1627762478,
|
||||||
remainingBlocks: 1121,
|
remainingBlocks: 1121,
|
||||||
remainingTime: 665977.6261244365,
|
remainingTime: 665977,
|
||||||
previousRetarget: -4.807005268478962
|
previousRetarget: -4.807005268478962,
|
||||||
|
nextRetargetHeight: 741888,
|
||||||
|
timeAvg: 302328,
|
||||||
|
timeOffset: 0
|
||||||
}`
|
}`
|
||||||
},
|
},
|
||||||
codeSampleSignet: {
|
codeSampleSignet: {
|
||||||
@ -140,11 +146,14 @@ export const restApiDocsData = [
|
|||||||
curl: [],
|
curl: [],
|
||||||
response: `{
|
response: `{
|
||||||
progressPercent: 44.397234501112074,
|
progressPercent: 44.397234501112074,
|
||||||
difficultyChange: 0.9845932018381687,
|
difficultyChange: 98.45932018381687,
|
||||||
estimatedRetargetDate: 1627762478.9111245,
|
estimatedRetargetDate: 1627762478,
|
||||||
remainingBlocks: 1121,
|
remainingBlocks: 1121,
|
||||||
remainingTime: 665977.6261244365,
|
remainingTime: 665977,
|
||||||
previousRetarget: -4.807005268478962
|
previousRetarget: -4.807005268478962,
|
||||||
|
nextRetargetHeight: 741888,
|
||||||
|
timeAvg: 302328,
|
||||||
|
timeOffset: 0
|
||||||
}`
|
}`
|
||||||
},
|
},
|
||||||
codeSampleLiquid: {
|
codeSampleLiquid: {
|
||||||
@ -153,11 +162,14 @@ export const restApiDocsData = [
|
|||||||
curl: [],
|
curl: [],
|
||||||
response: `{
|
response: `{
|
||||||
progressPercent: 44.397234501112074,
|
progressPercent: 44.397234501112074,
|
||||||
difficultyChange: 0.9845932018381687,
|
difficultyChange: 98.45932018381687,
|
||||||
estimatedRetargetDate: 1627762478.9111245,
|
estimatedRetargetDate: 1627762478,
|
||||||
remainingBlocks: 1121,
|
remainingBlocks: 1121,
|
||||||
remainingTime: 665977.6261244365,
|
remainingTime: 665977,
|
||||||
previousRetarget: -4.807005268478962
|
previousRetarget: -4.807005268478962,
|
||||||
|
nextRetargetHeight: 741888,
|
||||||
|
timeAvg: 302328,
|
||||||
|
timeOffset: 0
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user