Merge pull request #4680 from mempool/mononaut/sliding-difficulty

Project early difficulty adjustment with sliding window
This commit is contained in:
softsimon 2024-02-15 23:55:21 +08:00 committed by GitHub
commit 7fb699a02b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 95 additions and 27 deletions

View file

@ -11,9 +11,35 @@ describe('Mempool Difficulty Adjustment', () => {
};
const vectors = [
[ // Vector 1
[ // Vector 1 (normal adjustment)
[ // Inputs
dt('2024-02-02T15:42:06.000Z'), // Last DA time (in seconds)
dt('2024-02-08T14:43:05.000Z'), // timestamp of 504 blocks ago (in seconds)
dt('2024-02-11T22:43:01.000Z'), // Current time (now) (in seconds)
830027, // Current block height
7.333505241141637, // Previous retarget % (Passed through)
'mainnet', // Network (if testnet, next value is non-zero)
0, // Latest block timestamp in seconds (only used if difficulty already locked in)
],
{ // Expected Result
progressPercent: 71.97420634920636,
difficultyChange: 8.512745140778843,
estimatedRetargetDate: 1708004001715,
remainingBlocks: 565,
remainingTime: 312620715,
previousRetarget: 7.333505241141637,
previousTime: 1706888526,
nextRetargetHeight: 830592,
timeAvg: 553311,
adjustedTimeAvg: 553311,
timeOffset: 0,
expectedBlocks: 1338.0916666666667,
},
],
[ // Vector 2 (within quarter-epoch overlap)
[ // Inputs
dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds)
dt('2022-08-16T03:16:54.000Z'), // timestamp of 504 blocks ago (in seconds)
dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds)
750134, // Current block height
0.6280047707459726, // Previous retarget % (Passed through)
@ -22,21 +48,23 @@ describe('Mempool Difficulty Adjustment', () => {
],
{ // Expected Result
progressPercent: 9.027777777777777,
difficultyChange: 13.180707740199772,
estimatedRetargetDate: 1661895424692,
difficultyChange: 1.0420538959004633,
estimatedRetargetDate: 1662009048328,
remainingBlocks: 1834,
remainingTime: 977591692,
remainingTime: 1091215328,
previousRetarget: 0.6280047707459726,
previousTime: 1660820820,
nextRetargetHeight: 751968,
timeAvg: 533038,
adjustedTimeAvg: 594992,
timeOffset: 0,
expectedBlocks: 161.68833333333333,
},
],
[ // Vector 2 (testnet)
[ // Vector 3 (testnet)
[ // Inputs
dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds)
dt('2022-08-16T03:16:54.000Z'), // timestamp of 504 blocks ago (in seconds)
dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds)
750134, // Current block height
0.6280047707459726, // Previous retarget % (Passed through)
@ -45,22 +73,24 @@ describe('Mempool Difficulty Adjustment', () => {
],
{ // Expected Result is same other than timeOffset
progressPercent: 9.027777777777777,
difficultyChange: 13.180707740199772,
estimatedRetargetDate: 1661895424692,
difficultyChange: 1.0420538959004633,
estimatedRetargetDate: 1662009048328,
remainingBlocks: 1834,
remainingTime: 977591692,
remainingTime: 1091215328,
previousTime: 1660820820,
previousRetarget: 0.6280047707459726,
nextRetargetHeight: 751968,
timeAvg: 533038,
adjustedTimeAvg: 594992,
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
expectedBlocks: 161.68833333333333,
},
],
[ // Vector 3 (mainnet lock-in (epoch ending 788255))
[ // Vector 4 (mainnet lock-in (epoch ending 788255))
[ // Inputs
dt('2023-04-20T09:57:33.000Z'), // Last DA time (in seconds)
dt('2022-08-16T03:16:54.000Z'), // timestamp of 504 blocks ago (in seconds)
dt('2023-05-04T14:54:09.000Z'), // Current time (now) (in seconds)
788255, // Current block height
1.7220298879531821, // Previous retarget % (Passed through)
@ -77,16 +107,17 @@ describe('Mempool Difficulty Adjustment', () => {
previousTime: 1681984653,
nextRetargetHeight: 788256,
timeAvg: 609129,
adjustedTimeAvg: 609129,
timeOffset: 0,
expectedBlocks: 2045.66,
},
],
] as [[number, number, number, number, string, number], DifficultyAdjustment][];
] as [[number, 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.previousRetarget).toStrictEqual(vector[0][4]);
expect(result).toStrictEqual(vector[1]);
}
});

View file

@ -37,6 +37,7 @@ class Blocks {
private currentBits = 0;
private lastDifficultyAdjustmentTime = 0;
private previousDifficultyRetarget = 0;
private quarterEpochBlockTime: number | null = null;
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>)[] = [];
@ -775,6 +776,16 @@ class Blocks {
} else {
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
}
if (this.currentBlockHeight >= 503) {
try {
const quarterEpochBlockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight - 503);
const quarterEpochBlock = await bitcoinApi.$getBlock(quarterEpochBlockHash);
this.quarterEpochBlockTime = quarterEpochBlock?.timestamp;
} catch (e) {
this.quarterEpochBlockTime = null;
logger.warn('failed to update last epoch block time: ' + (e instanceof Error ? e.message : e));
}
}
if (blockHeightTip - this.currentBlockHeight > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 2) {
logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${config.MEMPOOL.INITIAL_BLOCKS_AMOUNT} recent blocks`);
@ -1308,6 +1319,10 @@ class Blocks {
return this.previousDifficultyRetarget;
}
public getQuarterEpochBlockTime(): number | null {
return this.quarterEpochBlockTime;
}
public getCurrentBlockHeight(): number {
return this.currentBlockHeight;
}

View file

@ -12,6 +12,7 @@ export interface DifficultyAdjustment {
previousTime: number; // Unix time in ms
nextRetargetHeight: number; // Block Height
timeAvg: number; // Duration of time in ms
adjustedTimeAvg; // Expected block interval with hashrate implied over last 504 blocks
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
expectedBlocks: number; // Block count
}
@ -80,6 +81,7 @@ export function calcBitsDifference(oldBits: number, newBits: number): number {
export function calcDifficultyAdjustment(
DATime: number,
quarterEpochTime: number | null,
nowSeconds: number,
blockHeight: number,
previousRetarget: number,
@ -100,8 +102,20 @@ export function calcDifficultyAdjustment(
let difficultyChange = 0;
let timeAvgSecs = blocksInEpoch ? diffSeconds / blocksInEpoch : BLOCK_SECONDS_TARGET;
let adjustedTimeAvgSecs = timeAvgSecs;
// for the first 504 blocks of the epoch, calculate the expected avg block interval
// from a sliding window over the last 504 blocks
if (quarterEpochTime && blocksInEpoch < 503) {
const timeLastEpoch = DATime - quarterEpochTime;
const adjustedTimeLastEpoch = timeLastEpoch * (1 + (previousRetarget / 100));
const adjustedTimeSpan = diffSeconds + adjustedTimeLastEpoch;
adjustedTimeAvgSecs = adjustedTimeSpan / 503;
difficultyChange = (BLOCK_SECONDS_TARGET / (adjustedTimeSpan / 504) - 1) * 100;
} else {
difficultyChange = (BLOCK_SECONDS_TARGET / (actualTimespan / (blocksInEpoch + 1)) - 1) * 100;
}
difficultyChange = (BLOCK_SECONDS_TARGET / (actualTimespan / (blocksInEpoch + 1)) - 1) * 100;
// Max increase is x4 (+300%)
if (difficultyChange > 300) {
difficultyChange = 300;
@ -126,7 +140,8 @@ export function calcDifficultyAdjustment(
}
const timeAvg = Math.floor(timeAvgSecs * 1000);
const remainingTime = remainingBlocks * timeAvg;
const adjustedTimeAvg = Math.floor(adjustedTimeAvgSecs * 1000);
const remainingTime = remainingBlocks * adjustedTimeAvg;
const estimatedRetargetDate = remainingTime + nowSeconds * 1000;
return {
@ -139,6 +154,7 @@ export function calcDifficultyAdjustment(
previousTime: DATime,
nextRetargetHeight,
timeAvg,
adjustedTimeAvg,
timeOffset,
expectedBlocks,
};
@ -155,9 +171,10 @@ class DifficultyAdjustmentApi {
return null;
}
const nowSeconds = Math.floor(new Date().getTime() / 1000);
const quarterEpochBlockTime = blocks.getQuarterEpochBlockTime();
return calcDifficultyAdjustment(
DATime, nowSeconds, blockHeight, previousRetarget,
DATime, quarterEpochBlockTime, nowSeconds, blockHeight, previousRetarget,
config.MEMPOOL.NETWORK, latestBlock.timestamp
);
}

View file

@ -14,7 +14,7 @@
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<div class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
@ -24,9 +24,6 @@
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
@ -49,13 +46,15 @@
<div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
<div class="card-text" i18n-ngbTooltip="mining.average-fee" [ngbTooltip]="halvingBlocksLeft" [tooltipContext]="{ epochData: epochData }" placement="bottom">
<span>{{ timeUntilHalving | date }}</span>
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<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>
<div class="symbol" *ngIf="blocksUntilHalving === 1; else approxTime">
<app-time kind="until" [time]="epochData.timeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
<app-time kind="until" [time]="epochData.adjustedTimeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
</div>
<ng-template #approxTime>
<div class="symbol">
<app-time kind="until" [time]="timeUntilHalving" [fastRender]="false" [fixedRender]="true" [precision]="0" [numUnits]="2" [units]="['year', 'day', 'hour', 'minute']"></app-time>
<span>{{ timeUntilHalving | date }}</span>
</div>
</ng-template>
</div>

View file

@ -16,6 +16,7 @@ interface EpochProgress {
blocksUntilHalving: number;
timeUntilHalving: number;
timeAvg: number;
adjustedTimeAvg: number;
}
@Component({
@ -85,6 +86,7 @@ export class DifficultyMiningComponent implements OnInit {
blocksUntilHalving: this.blocksUntilHalving,
timeUntilHalving: this.timeUntilHalving,
timeAvg: da.timeAvg,
adjustedTimeAvg: da.adjustedTimeAvg,
};
return data;
})

View file

@ -42,7 +42,7 @@
<div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
</div>
<div class="item">
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text bigger" [ngStyle]="{'color': epochData.colorAdjustments}">
<div class="card-text bigger" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
@ -52,9 +52,6 @@
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">

View file

@ -19,6 +19,7 @@ interface EpochProgress {
blocksUntilHalving: number;
timeUntilHalving: number;
timeAvg: number;
adjustedTimeAvg: number;
}
type BlockStatus = 'mined' | 'behind' | 'ahead' | 'next' | 'remaining';
@ -153,6 +154,7 @@ export class DifficultyComponent implements OnInit {
blocksUntilHalving,
timeUntilHalving,
timeAvg: da.timeAvg,
adjustedTimeAvg: da.adjustedTimeAvg,
};
return data;
})

View file

@ -34,7 +34,7 @@
<app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
</ng-template>
<ng-template #timeDiffMainnet>
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
<app-time kind="until" [time]="da.adjustedTimeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
</ng-template>
</div>
<ng-template #mergedBlock>

View file

@ -133,7 +133,7 @@
</ng-template>
<ng-template #timeEstimateDefault>
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.adjustedTimeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
<a *ngIf="!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
</span>
</ng-template>

View file

@ -155,6 +155,7 @@ export const restApiDocsData = [
previousRetarget: -4.807005268478962,
nextRetargetHeight: 741888,
timeAvg: 302328,
adjustedTimeAvg: 302328,
timeOffset: 0
}`
},
@ -171,6 +172,7 @@ export const restApiDocsData = [
previousRetarget: -4.807005268478962,
nextRetargetHeight: 741888,
timeAvg: 302328,
adjustedTimeAvg: 302328,
timeOffset: 0
}`
},
@ -187,6 +189,7 @@ export const restApiDocsData = [
previousRetarget: -4.807005268478962,
nextRetargetHeight: 741888,
timeAvg: 302328,
adjustedTimeAvg: 302328,
timeOffset: 0
}`
},
@ -203,6 +206,7 @@ export const restApiDocsData = [
previousRetarget: -4.807005268478962,
nextRetargetHeight: 741888,
timeAvg: 302328,
adjustedTimeAvg: 302328,
timeOffset: 0
}`
}

View file

@ -54,6 +54,7 @@ export interface DifficultyAdjustment {
previousTime: number;
nextRetargetHeight: number;
timeAvg: number;
adjustedTimeAvg: number;
timeOffset: number;
expectedBlocks: number;
}