Project early difficulty from sliding window

This commit is contained in:
Mononaut 2024-02-11 22:50:34 +00:00
parent cc2f42e814
commit 000524691a
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
10 changed files with 93 additions and 19 deletions

View File

@ -11,9 +11,35 @@ describe('Mempool Difficulty Adjustment', () => {
}; };
const vectors = [ 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 [ // Inputs
dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds) 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) dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds)
750134, // Current block height 750134, // Current block height
0.6280047707459726, // Previous retarget % (Passed through) 0.6280047707459726, // Previous retarget % (Passed through)
@ -22,21 +48,23 @@ describe('Mempool Difficulty Adjustment', () => {
], ],
{ // Expected Result { // Expected Result
progressPercent: 9.027777777777777, progressPercent: 9.027777777777777,
difficultyChange: 13.180707740199772, difficultyChange: 1.0420538959004633,
estimatedRetargetDate: 1661895424692, estimatedRetargetDate: 1662009048328,
remainingBlocks: 1834, remainingBlocks: 1834,
remainingTime: 977591692, remainingTime: 1091215328,
previousRetarget: 0.6280047707459726, previousRetarget: 0.6280047707459726,
previousTime: 1660820820, previousTime: 1660820820,
nextRetargetHeight: 751968, nextRetargetHeight: 751968,
timeAvg: 533038, timeAvg: 533038,
adjustedTimeAvg: 594992,
timeOffset: 0, timeOffset: 0,
expectedBlocks: 161.68833333333333, expectedBlocks: 161.68833333333333,
}, },
], ],
[ // Vector 2 (testnet) [ // Vector 3 (testnet)
[ // Inputs [ // Inputs
dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds) 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) dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds)
750134, // Current block height 750134, // Current block height
0.6280047707459726, // Previous retarget % (Passed through) 0.6280047707459726, // Previous retarget % (Passed through)
@ -45,22 +73,24 @@ describe('Mempool Difficulty Adjustment', () => {
], ],
{ // Expected Result is same other than timeOffset { // Expected Result is same other than timeOffset
progressPercent: 9.027777777777777, progressPercent: 9.027777777777777,
difficultyChange: 13.180707740199772, difficultyChange: 1.0420538959004633,
estimatedRetargetDate: 1661895424692, estimatedRetargetDate: 1662009048328,
remainingBlocks: 1834, remainingBlocks: 1834,
remainingTime: 977591692, remainingTime: 1091215328,
previousTime: 1660820820, previousTime: 1660820820,
previousRetarget: 0.6280047707459726, previousRetarget: 0.6280047707459726,
nextRetargetHeight: 751968, nextRetargetHeight: 751968,
timeAvg: 533038, timeAvg: 533038,
adjustedTimeAvg: 594992,
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only) 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 // If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
expectedBlocks: 161.68833333333333, expectedBlocks: 161.68833333333333,
}, },
], ],
[ // Vector 3 (mainnet lock-in (epoch ending 788255)) [ // Vector 4 (mainnet lock-in (epoch ending 788255))
[ // Inputs [ // Inputs
dt('2023-04-20T09:57:33.000Z'), // Last DA time (in seconds) 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) dt('2023-05-04T14:54:09.000Z'), // Current time (now) (in seconds)
788255, // Current block height 788255, // Current block height
1.7220298879531821, // Previous retarget % (Passed through) 1.7220298879531821, // Previous retarget % (Passed through)
@ -77,16 +107,17 @@ describe('Mempool Difficulty Adjustment', () => {
previousTime: 1681984653, previousTime: 1681984653,
nextRetargetHeight: 788256, nextRetargetHeight: 788256,
timeAvg: 609129, timeAvg: 609129,
adjustedTimeAvg: 609129,
timeOffset: 0, timeOffset: 0,
expectedBlocks: 2045.66, 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) { for (const vector of vectors) {
const result = calcDifficultyAdjustment(...vector[0]); const result = calcDifficultyAdjustment(...vector[0]);
// previousRetarget is passed through untouched // previousRetarget is passed through untouched
expect(result.previousRetarget).toStrictEqual(vector[0][3]); expect(result.previousRetarget).toStrictEqual(vector[0][4]);
expect(result).toStrictEqual(vector[1]); expect(result).toStrictEqual(vector[1]);
} }
}); });

View File

@ -37,6 +37,7 @@ class Blocks {
private currentBits = 0; private currentBits = 0;
private lastDifficultyAdjustmentTime = 0; private lastDifficultyAdjustmentTime = 0;
private previousDifficultyRetarget = 0; private previousDifficultyRetarget = 0;
private quarterEpochBlockTime: number | null = null;
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>)[] = []; private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>)[] = [];
@ -775,6 +776,16 @@ class Blocks {
} else { } else {
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height; 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) { 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`); 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; return this.previousDifficultyRetarget;
} }
public getQuarterEpochBlockTime(): number | null {
return this.quarterEpochBlockTime;
}
public getCurrentBlockHeight(): number { public getCurrentBlockHeight(): number {
return this.currentBlockHeight; return this.currentBlockHeight;
} }

View File

@ -12,6 +12,7 @@ export interface DifficultyAdjustment {
previousTime: number; // Unix time in ms previousTime: number; // Unix time in ms
nextRetargetHeight: number; // Block Height nextRetargetHeight: number; // Block Height
timeAvg: number; // Duration of time in ms 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 timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
expectedBlocks: number; // Block count expectedBlocks: number; // Block count
} }
@ -80,6 +81,7 @@ export function calcBitsDifference(oldBits: number, newBits: number): number {
export function calcDifficultyAdjustment( export function calcDifficultyAdjustment(
DATime: number, DATime: number,
quarterEpochTime: number | null,
nowSeconds: number, nowSeconds: number,
blockHeight: number, blockHeight: number,
previousRetarget: number, previousRetarget: number,
@ -100,8 +102,20 @@ export function calcDifficultyAdjustment(
let difficultyChange = 0; let difficultyChange = 0;
let timeAvgSecs = blocksInEpoch ? diffSeconds / blocksInEpoch : BLOCK_SECONDS_TARGET; 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%) // Max increase is x4 (+300%)
if (difficultyChange > 300) { if (difficultyChange > 300) {
difficultyChange = 300; difficultyChange = 300;
@ -126,7 +140,8 @@ export function calcDifficultyAdjustment(
} }
const timeAvg = Math.floor(timeAvgSecs * 1000); 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; const estimatedRetargetDate = remainingTime + nowSeconds * 1000;
return { return {
@ -139,6 +154,7 @@ export function calcDifficultyAdjustment(
previousTime: DATime, previousTime: DATime,
nextRetargetHeight, nextRetargetHeight,
timeAvg, timeAvg,
adjustedTimeAvg,
timeOffset, timeOffset,
expectedBlocks, expectedBlocks,
}; };
@ -155,9 +171,10 @@ class DifficultyAdjustmentApi {
return null; return null;
} }
const nowSeconds = Math.floor(new Date().getTime() / 1000); const nowSeconds = Math.floor(new Date().getTime() / 1000);
const quarterEpochBlockTime = blocks.getQuarterEpochBlockTime();
return calcDifficultyAdjustment( return calcDifficultyAdjustment(
DATime, nowSeconds, blockHeight, previousRetarget, DATime, quarterEpochBlockTime, nowSeconds, blockHeight, previousRetarget,
config.MEMPOOL.NETWORK, latestBlock.timestamp config.MEMPOOL.NETWORK, latestBlock.timestamp
); );
} }

View File

@ -49,13 +49,15 @@
<div class="item" *ngIf="showHalving"> <div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5> <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"> <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"> <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> </div>
<ng-template #approxTime> <ng-template #approxTime>
<div class="symbol"> <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> </div>
</ng-template> </ng-template>
</div> </div>

View File

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

View File

@ -19,6 +19,7 @@ interface EpochProgress {
blocksUntilHalving: number; blocksUntilHalving: number;
timeUntilHalving: number; timeUntilHalving: number;
timeAvg: number; timeAvg: number;
adjustedTimeAvg: number;
} }
type BlockStatus = 'mined' | 'behind' | 'ahead' | 'next' | 'remaining'; type BlockStatus = 'mined' | 'behind' | 'ahead' | 'next' | 'remaining';
@ -153,6 +154,7 @@ export class DifficultyComponent implements OnInit {
blocksUntilHalving, blocksUntilHalving,
timeUntilHalving, timeUntilHalving,
timeAvg: da.timeAvg, timeAvg: da.timeAvg,
adjustedTimeAvg: da.adjustedTimeAvg,
}; };
return data; 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> <app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
</ng-template> </ng-template>
<ng-template #timeDiffMainnet> <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> </ng-template>
</div> </div>
<ng-template #mergedBlock> <ng-template #mergedBlock>

View File

@ -133,7 +133,7 @@
</ng-template> </ng-template>
<ng-template #timeEstimateDefault> <ng-template #timeEstimateDefault>
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''"> <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> <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> </span>
</ng-template> </ng-template>

View File

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

View File

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