Merge pull request #1352 from mempool/simon/difficulty-adjustment-refactor

Difficulty adjustment refactor
This commit is contained in:
wiz 2022-03-14 16:33:55 +00:00 committed by GitHub
commit c4e5e45855
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 155 additions and 203 deletions

View File

@ -0,0 +1,71 @@
import config from '../config';
import { IDifficultyAdjustment } from '../mempool.interfaces';
import blocks from './blocks';
class DifficultyAdjustmentApi {
constructor() { }
public getDifficultyAdjustment(): IDifficultyAdjustment {
const DATime = blocks.getLastDifficultyAdjustmentTime();
const previousRetarget = blocks.getPreviousDifficultyRetarget();
const blockHeight = blocks.getCurrentBlockHeight();
const blocksCache = blocks.getBlocks();
const latestBlock = blocksCache[blocksCache.length - 1];
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;
}
}
const timeAvgDiff = difficultyChange * 0.1;
let timeAvgMins = 10;
if (timeAvgDiff > 0) {
timeAvgMins -= Math.abs(timeAvgDiff);
} else {
timeAvgMins += Math.abs(timeAvgDiff);
}
// Testnet difficulty is set to 1 after 20 minutes of no blockSize,
// therefore the time between blocks will always be below 20 minutes (1200s).
let timeOffset = 0;
if (config.MEMPOOL.NETWORK === 'testnet' && now - latestBlock.timestamp + timeAvgMins * 60 > 1200) {
timeOffset = -Math.min(now - latestBlock.timestamp, 1200) * 1000;
timeAvgMins = 20;
}
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,
};
}
}
export default new DifficultyAdjustmentApi();

View File

@ -12,6 +12,7 @@ import loadingIndicators from './loading-indicators';
import config from '../config';
import transactionUtils from './transaction-utils';
import rbfCache from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment';
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
@ -191,14 +192,13 @@ class WebsocketHandler {
return {
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
'previousRetarget': blocks.getPreviousDifficultyRetarget(),
'blocks': _blocks,
'conversions': fiatConversion.getConversionRates(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'transactions': memPool.getLatestTransactions(),
'backendInfo': backendInfo.getBackendInfo(),
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': difficultyAdjustment.getDifficultyAdjustment(),
...this.extraInitProperties
};
}
@ -234,6 +234,7 @@ class WebsocketHandler {
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment();
memPool.handleRbfTransactions(rbfTransactions);
this.wss.clients.forEach(async (client: WebSocket) => {
@ -247,6 +248,7 @@ class WebsocketHandler {
response['mempoolInfo'] = mempoolInfo;
response['vBytesPerSecond'] = vBytesPerSecond;
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
response['da'] = da;
}
if (client['want-mempool-blocks']) {
@ -410,8 +412,7 @@ class WebsocketHandler {
const response = {
'block': block,
'mempoolInfo': memPool.getMempoolInfo(),
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
'previousRetarget': blocks.getPreviousDifficultyRetarget(),
'da': difficultyAdjustment.getDifficultyAdjustment(),
};
if (mBlocks && client['want-mempool-blocks']) {

View File

@ -197,3 +197,15 @@ export interface IBackendInfo {
gitCommit: string;
version: string;
}
export interface IDifficultyAdjustment {
progressPercent: number;
difficultyChange: number;
estimatedRetargetDate: number;
remainingBlocks: number;
remainingTime: number;
previousRetarget: number;
nextRetargetHeight: number;
timeAvg: number;
timeOffset: number;
}

View File

@ -25,6 +25,7 @@ import axios from 'axios';
import mining from './api/mining';
import BlocksRepository from './repositories/BlocksRepository';
import HashratesRepository from './repositories/HashratesRepository';
import difficultyAdjustment from './api/difficulty-adjustment';
class Routes {
constructor() {}
@ -847,55 +848,7 @@ class Routes {
public getDifficultyChange(req: Request, res: Response) {
try {
const DATime = blocks.getLastDifficultyAdjustmentTime();
const previousRetarget = blocks.getPreviousDifficultyRetarget();
const blockHeight = blocks.getCurrentBlockHeight();
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;
}
}
const timeAvgDiff = difficultyChange * 0.1;
let timeAvgMins = 10;
if (timeAvgDiff > 0) {
timeAvgMins -= Math.abs(timeAvgDiff);
} else {
timeAvgMins += Math.abs(timeAvgDiff);
}
const timeAvg = timeAvgMins * 60;
const remainingTime = remainingBlocks * timeAvg;
const estimatedRetargetDate = remainingTime + now;
const result = {
progressPercent,
difficultyChange,
estimatedRetargetDate,
remainingBlocks,
remainingTime,
previousRetarget,
nextRetargetHeight,
timeAvg,
};
res.json(result);
res.json(difficultyAdjustment.getDifficultyAdjustment());
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}

View File

@ -6,12 +6,11 @@ import { StateService } from '../..//services/state.service';
interface EpochProgress {
base: string;
change: number;
progress: string;
progress: number;
remainingBlocks: number;
newDifficultyHeight: number;
colorAdjustments: string;
colorPreviousAdjustments: string;
timeAvg: string;
remainingTime: number;
previousRetarget: number;
blocksUntilHalving: number;
@ -38,85 +37,52 @@ export class DifficultyComponent implements OnInit {
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.difficultyEpoch$ = timer(0, 1000)
.pipe(
switchMap(() => combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)),
this.stateService.lastDifficultyAdjustment$,
this.stateService.previousRetarget$
])),
map(([block, DATime, previousRetarget]) => {
const now = new Date().getTime() / 1000;
const diff = now - DATime;
const blocksInEpoch = block.height % 2016;
const progress = (blocksInEpoch >= 0) ? (blocksInEpoch / 2016 * 100).toFixed(2) : `100`;
const remainingBlocks = 2016 - blocksInEpoch;
const newDifficultyHeight = block.height + remainingBlocks;
this.difficultyEpoch$ = combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)),
this.stateService.difficultyAdjustment$,
])
.pipe(
map(([block, da]) => {
let colorAdjustments = '#ffffff66';
if (da.difficultyChange > 0) {
colorAdjustments = '#3bcc49';
}
if (da.difficultyChange < 0) {
colorAdjustments = '#dc3545';
}
let change = 0;
if (remainingBlocks < 1870) {
if (blocksInEpoch > 0) {
change = (600 / (diff / blocksInEpoch ) - 1) * 100;
}
if (change > 300) {
change = 300;
}
if (change < -75) {
change = -75;
}
let colorPreviousAdjustments = '#dc3545';
if (da.previousRetarget) {
if (da.previousRetarget >= 0) {
colorPreviousAdjustments = '#3bcc49';
}
const timeAvgDiff = change * 0.1;
let timeAvgMins = 10;
if (timeAvgDiff > 0) {
timeAvgMins -= Math.abs(timeAvgDiff);
} else {
timeAvgMins += Math.abs(timeAvgDiff);
}
const timeAvg = timeAvgMins.toFixed(0);
const remainingTime = (remainingBlocks * timeAvgMins * 60 * 1000) + (now * 1000);
let colorAdjustments = '#ffffff66';
if (change > 0) {
colorAdjustments = '#3bcc49';
}
if (change < 0) {
colorAdjustments = '#dc3545';
}
let colorPreviousAdjustments = '#dc3545';
if (previousRetarget) {
if (previousRetarget >= 0) {
colorPreviousAdjustments = '#3bcc49';
}
if (previousRetarget === 0) {
colorPreviousAdjustments = '#ffffff66';
}
} else {
if (da.previousRetarget === 0) {
colorPreviousAdjustments = '#ffffff66';
}
} else {
colorPreviousAdjustments = '#ffffff66';
}
const blocksUntilHalving = 210000 - (block.height % 210000);
const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000);
const timeAvgMins = da.timeAvg;
const now = new Date().getTime() / 1000;
const blocksUntilHalving = 210000 - (block.height % 210000);
const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000);
return {
base: `${progress}%`,
change,
progress,
remainingBlocks,
timeAvg,
colorAdjustments,
colorPreviousAdjustments,
blocksInEpoch,
newDifficultyHeight,
remainingTime,
previousRetarget,
blocksUntilHalving,
timeUntilHalving,
};
})
);
const data = {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
remainingBlocks: da.remainingBlocks,
colorAdjustments,
colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight,
remainingTime: da.remainingTime,
previousRetarget: da.previousRetarget,
blocksUntilHalving,
timeUntilHalving,
};
return data;
})
);
}
}

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks">
<div class="mempool-blocks-container" *ngIf="(timeAvg$ | async) as timeAvg;">
<div class="mempool-blocks-container" *ngIf="(difficultyAdjustments$ | async) as da;">
<div class="flashing">
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
<div class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
@ -26,7 +26,7 @@
<app-time-until [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time-until>
</ng-template>
<ng-template #timeDiffMainnet>
<app-time-until [time]="(timeAvg * i) + now + timeAvg + timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time-until>
<app-time-until [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time-until>
</ng-template>
</div>
<ng-template #mergedBlock>

View File

@ -8,6 +8,7 @@ import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
import { specialBlocks } from 'src/app/app.constants';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { Location } from '@angular/common';
import { DifficultyAdjustment } from 'src/app/interfaces/node-api.interface';
@Component({
selector: 'app-mempool-blocks',
@ -20,7 +21,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
mempoolBlocks: MempoolBlock[] = [];
mempoolEmptyBlocks: MempoolBlock[] = this.mountEmptyBlocks();
mempoolBlocks$: Observable<MempoolBlock[]>;
timeAvg$: Observable<number>;
difficultyAdjustments$: Observable<DifficultyAdjustment>;
loadingBlocks$: Observable<boolean>;
blocksSubscription: Subscription;
@ -123,40 +124,11 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
})
);
this.timeAvg$ = timer(0, 1000)
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
.pipe(
switchMap(() => combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)),
this.stateService.lastDifficultyAdjustment$
])),
map(([block, DATime]) => {
map((da) => {
this.now = new Date().getTime();
const now = new Date().getTime() / 1000;
const diff = now - DATime;
const blocksInEpoch = block.height % 2016;
let difficultyChange = 0;
if (blocksInEpoch > 0) {
difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100;
}
const timeAvgDiff = difficultyChange * 0.1;
let timeAvgMins = 10;
if (timeAvgDiff > 0 ){
timeAvgMins -= Math.abs(timeAvgDiff);
} else {
timeAvgMins += Math.abs(timeAvgDiff);
}
// testnet difficulty is set to 1 after 20 minutes of no blockSize
// therefore the time between blocks will always be below 20 minutes (1200s)
if (this.stateService.network === 'testnet' && now - block.timestamp + timeAvgMins * 60 > 1200) {
this.timeOffset = -Math.min(now - block.timestamp, 1200) * 1000;
timeAvgMins = 20;
} else {
this.timeOffset = 0;
}
return timeAvgMins * 60 * 1000;
return da;
})
);

View File

@ -66,29 +66,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
this.timeAvg$ = timer(0, 1000)
.pipe(
switchMap(() => combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)),
this.stateService.lastDifficultyAdjustment$
])),
map(([block, DATime]) => {
this.now = new Date().getTime();
const now = new Date().getTime() / 1000;
const diff = now - DATime;
const blocksInEpoch = block.height % 2016;
let difficultyChange = 0;
if (blocksInEpoch > 0) {
difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100;
}
const timeAvgDiff = difficultyChange * 0.1;
let timeAvgMins = 10;
if (timeAvgDiff > 0 ){
timeAvgMins -= Math.abs(timeAvgDiff);
} else {
timeAvgMins += Math.abs(timeAvgDiff);
}
return timeAvgMins * 60 * 1000;
})
switchMap(() => this.stateService.difficultyAdjustment$),
map((da) => da.timeAvg)
);
this.fetchCpfpSubscription = this.fetchCpfp$

View File

@ -26,12 +26,15 @@ export interface CpfpInfo {
}
export interface DifficultyAdjustment {
progressPercent: number;
difficultyChange: number;
estimatedRetargetDate: number;
previousRetarget: number;
progressPercent: number;
remainingBlocks: number;
remainingTime: number;
previousRetarget: number;
nextRetargetHeight: number;
timeAvg: number;
timeOffset: number;
}
export interface AddressInformation {
@ -111,4 +114,3 @@ export interface BlockExtension {
export interface BlockExtended extends Block {
extras?: BlockExtension;
}

View File

@ -1,6 +1,6 @@
import { ILoadingIndicators } from '../services/state.service';
import { Transaction } from './electrs.interface';
import { BlockExtended } from './node-api.interface';
import { BlockExtended, DifficultyAdjustment } from './node-api.interface';
export interface WebsocketResponse {
block?: BlockExtended;
@ -10,7 +10,6 @@ export interface WebsocketResponse {
historicalDate?: string;
mempoolInfo?: MempoolInfo;
vBytesPerSecond?: number;
lastDifficultyAdjustment?: number;
previousRetarget?: number;
action?: string;
data?: string[];
@ -21,6 +20,7 @@ export interface WebsocketResponse {
transactions?: TransactionStripped[];
loadingIndicators?: ILoadingIndicators;
backendInfo?: IBackendInfo;
da?: DifficultyAdjustment;
'track-tx'?: string;
'track-address'?: string;
'track-asset'?: string;

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs, ITranslators, PoolsStats, PoolStat, BlockExtended, BlockExtension } from '../interfaces/node-api.interface';
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolsStats, PoolStat, BlockExtended } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs';
import { StateService } from './state.service';
import { WebsocketResponse } from '../interfaces/websocket.interface';
@ -105,10 +105,6 @@ export class ApiService {
return this.httpClient.get<CpfpInfo>(this.apiBaseUrl + this.apiBasePath + '/api/v1/cpfp/' + txid);
}
getDifficultyAdjustment$(): Observable<DifficultyAdjustment> {
return this.httpClient.get<DifficultyAdjustment>(this.apiBaseUrl + this.apiBasePath + '/api/v1/difficulty-adjustment');
}
validateAddress$(address: string): Observable<AddressInformation> {
return this.httpClient.get<AddressInformation>(this.apiBaseUrl + this.apiBasePath + '/api/v1/validate-address/' + address);
}

View File

@ -2,7 +2,7 @@ import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { IBackendInfo, MempoolBlock, MempoolInfo, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { BlockExtended, DifficultyAdjustment, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { map, shareReplay } from 'rxjs/operators';
@ -82,11 +82,11 @@ export class StateService {
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
txReplaced$ = new Subject<ReplacedTransaction>();
utxoSpent$ = new Subject<object>();
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
mempoolTransactions$ = new Subject<Transaction>();
blockTransactions$ = new Subject<Transaction>();
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
vbytesPerSecond$ = new ReplaySubject<number>(1);
lastDifficultyAdjustment$ = new ReplaySubject<number>(1);
previousRetarget$ = new ReplaySubject<number>(1);
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);

View File

@ -259,6 +259,10 @@ export class WebsocketService {
this.stateService.utxoSpent$.next(response.utxoSpent);
}
if (response.da) {
this.stateService.difficultyAdjustment$.next(response.da);
}
if (response.backendInfo) {
this.stateService.backendInfo$.next(response.backendInfo);
@ -301,10 +305,6 @@ export class WebsocketService {
this.stateService.vbytesPerSecond$.next(response.vBytesPerSecond);
}
if (response.lastDifficultyAdjustment !== undefined) {
this.stateService.lastDifficultyAdjustment$.next(response.lastDifficultyAdjustment);
}
if (response.previousRetarget !== undefined) {
this.stateService.previousRetarget$.next(response.previousRetarget);
}