Project early difficulty from sliding window
This commit is contained in:
		
							parent
							
								
									cc2f42e814
								
							
						
					
					
						commit
						000524691a
					
				| @ -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]); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
|  | ||||
| @ -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 | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -49,13 +49,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> | ||||
|  | ||||
| @ -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; | ||||
|       }) | ||||
|  | ||||
| @ -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; | ||||
|       }) | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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 | ||||
| }` | ||||
|         } | ||||
|  | ||||
| @ -54,6 +54,7 @@ export interface DifficultyAdjustment { | ||||
|   previousTime: number; | ||||
|   nextRetargetHeight: number; | ||||
|   timeAvg: number; | ||||
|   adjustedTimeAvg: number; | ||||
|   timeOffset: number; | ||||
|   expectedBlocks: number; | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user