Multi-pool ETA calculation
This commit is contained in:
		
							parent
							
								
									6277813414
								
							
						
					
					
						commit
						833418514e
					
				
							
								
								
									
										170
									
								
								frontend/src/app/services/eta.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								frontend/src/app/services/eta.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,170 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { AccelerationPosition, CpfpInfo, DifficultyAdjustment, MempoolPosition, SinglePoolStats } from '../interfaces/node-api.interface'; | ||||
| import { StateService } from './state.service'; | ||||
| import { MempoolBlock } from '../interfaces/websocket.interface'; | ||||
| import { Transaction } from '../interfaces/electrs.interface'; | ||||
| import { MiningStats } from './mining.service'; | ||||
| import { getUnacceleratedFeeRate } from '../shared/transaction.utils'; | ||||
| 
 | ||||
| export interface ETA { | ||||
|   now: number, // time at which calculation performed
 | ||||
|   time: number, // absolute time expected (in unix epoch ms)
 | ||||
|   wait: number, // expected wait time in ms
 | ||||
|   blocks: number, // expected number of blocks (rounded up to next integer)
 | ||||
| } | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| }) | ||||
| export class EtaService { | ||||
|   constructor( | ||||
|     private stateService: StateService, | ||||
|   ) { } | ||||
| 
 | ||||
|   mempoolPositionFromFees(feerate: number, mempoolBlocks: MempoolBlock[]): MempoolPosition { | ||||
|     for (let txInBlockIndex = 0; txInBlockIndex < mempoolBlocks.length; txInBlockIndex++) { | ||||
|       const block = mempoolBlocks[txInBlockIndex]; | ||||
|       for (let i = 0; i < block.feeRange.length - 1; i++) { | ||||
|         if (feerate < block.feeRange[i + 1] && feerate >= block.feeRange[i]) { | ||||
|           const feeRangeIndex = i; | ||||
|           const feeRangeChunkSize = 1 / (block.feeRange.length - 1); | ||||
| 
 | ||||
|           const txFee = feerate - block.feeRange[i]; | ||||
|           const max = block.feeRange[i + 1] - block.feeRange[i]; | ||||
|           const blockLocation = txFee / max; | ||||
| 
 | ||||
|           const chunkPositionOffset = blockLocation * feeRangeChunkSize; | ||||
|           const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset; | ||||
| 
 | ||||
|           const blockedFilledPercentage = (block.blockVSize > this.stateService.blockVSize ? this.stateService.blockVSize : block.blockVSize) / this.stateService.blockVSize; | ||||
| 
 | ||||
|           return { | ||||
|             block: txInBlockIndex, | ||||
|             vsize: (1 - feePosition) * blockedFilledPercentage * this.stateService.blockVSize, | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       if (feerate >= block.feeRange[block.feeRange.length - 1]) { | ||||
|         // at the very front of this block
 | ||||
|         return { | ||||
|           block: txInBlockIndex, | ||||
|           vsize: 0, | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     // at the very back of the last block
 | ||||
|     return { | ||||
|       block: mempoolBlocks.length - 1, | ||||
|       vsize: mempoolBlocks[mempoolBlocks.length - 1].blockVSize, | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   calculateETA( | ||||
|     network: string, | ||||
|     tx: Transaction, | ||||
|     mempoolBlocks: MempoolBlock[], | ||||
|     position: { txid: string, position: MempoolPosition, cpfp: CpfpInfo | null, accelerationPositions?: AccelerationPosition[] }, | ||||
|     da: DifficultyAdjustment, | ||||
|     miningStats: MiningStats, | ||||
|     isAccelerated: boolean, | ||||
|     accelerationPositions: AccelerationPosition[], | ||||
|   ): ETA | null { | ||||
|     // return this.calculateETA(tx, this.accelerationPositions, position, mempoolBlocks, da, isAccelerated)
 | ||||
|     if (!tx || !mempoolBlocks) { | ||||
|       return null; | ||||
|     } | ||||
|     const now = Date.now(); | ||||
| 
 | ||||
|     // use known projected position, or fall back to feerate-based estimate
 | ||||
|     const mempoolPosition = position?.position ?? this.mempoolPositionFromFees(tx.effectiveFeePerVsize || tx.feePerVsize, mempoolBlocks); | ||||
|     if (!mempoolPosition) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     // Liquid block time is always 60 seconds
 | ||||
|     if (network === 'liquid' || network === 'liquidtestnet') { | ||||
|       return { | ||||
|         now, | ||||
|         time: now + (60_000 * (mempoolPosition.block + 1)), | ||||
|         wait: (60_000 * (mempoolPosition.block + 1)), | ||||
|         blocks: mempoolPosition.block + 1, | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // difficulty adjustment estimate is required to know avg block time on non-Liquid networks
 | ||||
|     if (!da) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     if (!isAccelerated) { | ||||
|       const blocks = mempoolPosition.block + 1; | ||||
|       const wait = da.adjustedTimeAvg * (mempoolPosition.block + 1); | ||||
|       return { | ||||
|         now, | ||||
|         time: wait + now + da.timeOffset, | ||||
|         wait, | ||||
|         blocks, | ||||
|       } | ||||
|     } else { | ||||
|       // accelerated transactions
 | ||||
| 
 | ||||
|       // mining stats are required for pool hashrate weightings
 | ||||
|       if (!miningStats) { | ||||
|         return null; | ||||
|       } | ||||
|       // acceleration positions are required
 | ||||
|       if (!accelerationPositions) { | ||||
|         return null; | ||||
|       } | ||||
| 
 | ||||
|       /** | ||||
|        *  **Define parameters** | ||||
|             - Let $\{C_i\}$ be the set of pools. | ||||
|             - $P(C_i)$ is the probability that a random block belongs to pool $C_i$. | ||||
|             - $N(C_i)$ is the number of blocks that need to be mined before a block by pool $C_i$ contains the given transaction. | ||||
|             - $H(n)$ is the proportion of hashrate for which the transaction is in mempool block ≤ $n$ | ||||
|             - $S(n)$ is the probability of the transaction being mined in block $n$ | ||||
|               - by definition, $S(max) = 1$ , where $max$ is the maximum depth of the transaction in any mempool, and therefore $S(n>max) = 0$ | ||||
|             - $Q$ is the expected number of blocks before the transaction is confirmed | ||||
|             - $E$ is the expected time before the transaction is confirmed | ||||
|           **Overall expected confirmation time** | ||||
|             - $S(i) = H(i) \times (1 - \sum_{j=0}^{i-1} S(j))$ | ||||
|               - the probability of mining a block including the transaction at this depth, multiplied by the probability that it hasn't already been mined at an earlier depth. | ||||
|             - $Q = \sum_{i=0}^{max} S(i) \times (i+1)$ | ||||
|               - number of blocks, weighted by the probability that the block includes the transaction | ||||
|             - $E = Q \times T$ | ||||
|               - expected number of blocks, multiplied by the avg time per block | ||||
|        */ | ||||
|       const pools: { [id: number]: SinglePoolStats } = {}; | ||||
|       for (const pool of miningStats.pools) { | ||||
|         pools[pool.poolUniqueId] = pool; | ||||
|       } | ||||
|       const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks); | ||||
|       const positions = [unacceleratedPosition, ...accelerationPositions]; | ||||
|       const max = unacceleratedPosition.block; // by definition, assuming no negative fee deltas or out of band txs
 | ||||
| 
 | ||||
|       let tailProb = 0; | ||||
|       let Q = 0; | ||||
|       for (let i = 0; i < max; i++) { | ||||
|         // find H_i
 | ||||
|         const H = accelerationPositions.reduce((total, pos) => total + (pos.block <= i ? pools[pos.poolId].lastEstimatedHashrate : 0), 0) / miningStats.lastEstimatedHashrate; | ||||
|         // find S_i
 | ||||
|         let S = H * (1 - tailProb); | ||||
|         // accumulate sum (S_i x i)
 | ||||
|         Q += (S * (i + 1)); | ||||
|         // accumulate sum (S_j)
 | ||||
|         tailProb += S; | ||||
|       } | ||||
|       // at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already
 | ||||
|       Q += (1-tailProb); | ||||
|       const eta = da.timeAvg * Q; // T x Q
 | ||||
| 
 | ||||
|       return { | ||||
|         now, | ||||
|         time: eta + now + da.timeOffset, | ||||
|         wait: eta, | ||||
|         blocks: Math.ceil(eta / da.adjustedTimeAvg), | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -442,4 +442,18 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac | ||||
|   } | ||||
| 
 | ||||
|   return flags; | ||||
| } | ||||
| 
 | ||||
| export function getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean): number { | ||||
|   if (accelerated) { | ||||
|     let ancestorVsize = tx.weight / 4; | ||||
|     let ancestorFee = tx.fee; | ||||
|     for (const ancestor of tx.ancestors || []) { | ||||
|       ancestorVsize += (ancestor.weight / 4); | ||||
|       ancestorFee += ancestor.fee; | ||||
|     } | ||||
|     return Math.min(tx.fee / (tx.weight / 4), (ancestorFee / ancestorVsize)); | ||||
|   } else { | ||||
|     return tx.effectiveFeePerVsize; | ||||
|   } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user