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