Merge pull request #3315 from mempool/mononaut/effective-fee-rates
Use effective fee rate heuristics for block fee span
This commit is contained in:
		
						commit
						5ba2c181b0
					
				| @ -2,7 +2,7 @@ import config from '../config'; | |||||||
| import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; | import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; | ||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
| import memPool from './mempool'; | import memPool from './mempool'; | ||||||
| import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo } from '../mempool.interfaces'; | import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo, CpfpSummary } from '../mempool.interfaces'; | ||||||
| import { Common } from './common'; | import { Common } from './common'; | ||||||
| import diskCache from './disk-cache'; | import diskCache from './disk-cache'; | ||||||
| import transactionUtils from './transaction-utils'; | import transactionUtils from './transaction-utils'; | ||||||
| @ -200,8 +200,15 @@ class Blocks { | |||||||
|       extras.segwitTotalWeight = 0; |       extras.segwitTotalWeight = 0; | ||||||
|     } else { |     } else { | ||||||
|       const stats: IBitcoinApi.BlockStats = await bitcoinClient.getBlockStats(block.id); |       const stats: IBitcoinApi.BlockStats = await bitcoinClient.getBlockStats(block.id); | ||||||
|       extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles
 |       let feeStats = { | ||||||
|       extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(); |         medianFee: stats.feerate_percentiles[2], // 50th percentiles
 | ||||||
|  |         feeRange: [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(), | ||||||
|  |       }; | ||||||
|  |       if (transactions?.length > 1) { | ||||||
|  |         feeStats = Common.calcEffectiveFeeStatistics(transactions); | ||||||
|  |       } | ||||||
|  |       extras.medianFee = feeStats.medianFee; | ||||||
|  |       extras.feeRange = feeStats.feeRange; | ||||||
|       extras.totalFees = stats.totalfee; |       extras.totalFees = stats.totalfee; | ||||||
|       extras.avgFee = stats.avgfee; |       extras.avgFee = stats.avgfee; | ||||||
|       extras.avgFeeRate = stats.avgfeerate; |       extras.avgFeeRate = stats.avgfeerate; | ||||||
| @ -571,7 +578,8 @@ class Blocks { | |||||||
|       const block = BitcoinApi.convertBlock(verboseBlock); |       const block = BitcoinApi.convertBlock(verboseBlock); | ||||||
|       const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash); |       const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash); | ||||||
|       const transactions = await this.$getTransactionsExtended(blockHash, block.height, false); |       const transactions = await this.$getTransactionsExtended(blockHash, block.height, false); | ||||||
|       const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions); |       const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions); | ||||||
|  |       const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); | ||||||
|       const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock); |       const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock); | ||||||
| 
 | 
 | ||||||
|       // start async callbacks
 |       // start async callbacks
 | ||||||
| @ -619,7 +627,7 @@ class Blocks { | |||||||
|             await this.$getStrippedBlockTransactions(blockExtended.id, true); |             await this.$getStrippedBlockTransactions(blockExtended.id, true); | ||||||
|           } |           } | ||||||
|           if (config.MEMPOOL.CPFP_INDEXING) { |           if (config.MEMPOOL.CPFP_INDEXING) { | ||||||
|             this.$indexCPFP(blockExtended.id, this.currentBlockHeight); |             this.$saveCpfp(blockExtended.id, this.currentBlockHeight, cpfpSummary); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @ -913,42 +921,20 @@ class Blocks { | |||||||
|   public async $indexCPFP(hash: string, height: number): Promise<void> { |   public async $indexCPFP(hash: string, height: number): Promise<void> { | ||||||
|     const block = await bitcoinClient.getBlock(hash, 2); |     const block = await bitcoinClient.getBlock(hash, 2); | ||||||
|     const transactions = block.tx.map(tx => { |     const transactions = block.tx.map(tx => { | ||||||
|       tx.vsize = tx.weight / 4; |  | ||||||
|       tx.fee *= 100_000_000; |       tx.fee *= 100_000_000; | ||||||
|       return tx; |       return tx; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const clusters: any[] = []; |     const summary = Common.calculateCpfp(height, transactions); | ||||||
| 
 | 
 | ||||||
|     let cluster: TransactionStripped[] = []; |     await this.$saveCpfp(hash, height, summary); | ||||||
|     let ancestors: { [txid: string]: boolean } = {}; | 
 | ||||||
|     for (let i = transactions.length - 1; i >= 0; i--) { |     const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions); | ||||||
|       const tx = transactions[i]; |     await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats); | ||||||
|       if (!ancestors[tx.txid]) { |  | ||||||
|         let totalFee = 0; |  | ||||||
|         let totalVSize = 0; |  | ||||||
|         cluster.forEach(tx => { |  | ||||||
|           totalFee += tx?.fee || 0; |  | ||||||
|           totalVSize += tx.vsize; |  | ||||||
|         }); |  | ||||||
|         const effectiveFeePerVsize = totalFee / totalVSize; |  | ||||||
|         if (cluster.length > 1) { |  | ||||||
|           clusters.push({ |  | ||||||
|             root: cluster[0].txid, |  | ||||||
|             height, |  | ||||||
|             txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: tx.fee || 0 }; }), |  | ||||||
|             effectiveFeePerVsize, |  | ||||||
|           }); |  | ||||||
|   } |   } | ||||||
|         cluster = []; | 
 | ||||||
|         ancestors = {}; |   public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> { | ||||||
|       } |     const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters); | ||||||
|       cluster.push(tx); |  | ||||||
|       tx.vin.forEach(vin => { |  | ||||||
|         ancestors[vin.txid] = true; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     const result = await cpfpRepository.$batchSaveClusters(clusters); |  | ||||||
|     if (!result) { |     if (!result) { | ||||||
|       await cpfpRepository.$insertProgressMarker(height); |       await cpfpRepository.$insertProgressMarker(height); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { CpfpInfo, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces'; | import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces'; | ||||||
| import config from '../config'; | import config from '../config'; | ||||||
| import { NodeSocket } from '../repositories/NodesSocketsRepository'; | import { NodeSocket } from '../repositories/NodesSocketsRepository'; | ||||||
| import { isIP } from 'net'; | import { isIP } from 'net'; | ||||||
| @ -345,4 +345,99 @@ export class Common { | |||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   static calculateCpfp(height: number, transactions: TransactionExtended[]): CpfpSummary { | ||||||
|  |     const clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[] = []; | ||||||
|  |     let cluster: TransactionExtended[] = []; | ||||||
|  |     let ancestors: { [txid: string]: boolean } = {}; | ||||||
|  |     const txMap = {}; | ||||||
|  |     for (let i = transactions.length - 1; i >= 0; i--) { | ||||||
|  |       const tx = transactions[i]; | ||||||
|  |       txMap[tx.txid] = tx; | ||||||
|  |       if (!ancestors[tx.txid]) { | ||||||
|  |         let totalFee = 0; | ||||||
|  |         let totalVSize = 0; | ||||||
|  |         cluster.forEach(tx => { | ||||||
|  |           totalFee += tx?.fee || 0; | ||||||
|  |           totalVSize += (tx.weight / 4); | ||||||
|  |         }); | ||||||
|  |         const effectiveFeePerVsize = totalFee / totalVSize; | ||||||
|  |         if (cluster.length > 1) { | ||||||
|  |           clusters.push({ | ||||||
|  |             root: cluster[0].txid, | ||||||
|  |             height, | ||||||
|  |             txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }), | ||||||
|  |             effectiveFeePerVsize, | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |         cluster.forEach(tx => { | ||||||
|  |           txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; | ||||||
|  |         }); | ||||||
|  |         cluster = []; | ||||||
|  |         ancestors = {}; | ||||||
|  |       } | ||||||
|  |       cluster.push(tx); | ||||||
|  |       tx.vin.forEach(vin => { | ||||||
|  |         ancestors[vin.txid] = true; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |       transactions, | ||||||
|  |       clusters, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string }[]): EffectiveFeeStats { | ||||||
|  |     const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate); | ||||||
|  | 
 | ||||||
|  |     let weightCount = 0; | ||||||
|  |     let medianFee = 0; | ||||||
|  |     let medianWeight = 0; | ||||||
|  | 
 | ||||||
|  |     // calculate the "medianFee" as the average fee rate of the middle 10000 weight units of transactions
 | ||||||
|  |     const leftBound = 1995000; | ||||||
|  |     const rightBound = 2005000; | ||||||
|  |     for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) { | ||||||
|  |       const left = weightCount; | ||||||
|  |       const right = weightCount + sortedTxs[i].weight; | ||||||
|  |       if (right > leftBound) { | ||||||
|  |         const weight = Math.min(right, rightBound) - Math.max(left, leftBound); | ||||||
|  |         medianFee += (sortedTxs[i].rate * (weight / 4) ); | ||||||
|  |         medianWeight += weight; | ||||||
|  |       } | ||||||
|  |       weightCount += sortedTxs[i].weight; | ||||||
|  |     } | ||||||
|  |     const medianFeeRate = medianWeight ? (medianFee / (medianWeight / 4)) : 0; | ||||||
|  | 
 | ||||||
|  |     // minimum effective fee heuristic:
 | ||||||
|  |     // lowest of
 | ||||||
|  |     // a) the 1st percentile of effective fee rates
 | ||||||
|  |     // b) the minimum effective fee rate in the last 2% of transactions (in block order)
 | ||||||
|  |     const minFee = Math.min( | ||||||
|  |       Common.getNthPercentile(1, sortedTxs).rate, | ||||||
|  |       transactions.slice(-transactions.length / 50).reduce((min, tx) => { return Math.min(min, tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4))); }, Infinity) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     // maximum effective fee heuristic:
 | ||||||
|  |     // highest of
 | ||||||
|  |     // a) the 99th percentile of effective fee rates
 | ||||||
|  |     // b) the maximum effective fee rate in the first 2% of transactions (in block order)
 | ||||||
|  |     const maxFee = Math.max( | ||||||
|  |       Common.getNthPercentile(99, sortedTxs).rate, | ||||||
|  |       transactions.slice(0, transactions.length / 50).reduce((max, tx) => { return Math.max(max, tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4))); }, 0) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       medianFee: medianFeeRate, | ||||||
|  |       feeRange: [ | ||||||
|  |         minFee, | ||||||
|  |         [10,25,50,75,90].map(n => Common.getNthPercentile(n, sortedTxs).rate), | ||||||
|  |         maxFee, | ||||||
|  |       ].flat(), | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static getNthPercentile(n: number, sortedDistribution: any[]): any { | ||||||
|  |     return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))]; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -214,6 +214,16 @@ export interface MempoolStats { | |||||||
|   tx_count: number; |   tx_count: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface EffectiveFeeStats { | ||||||
|  |   medianFee: number; // median effective fee rate
 | ||||||
|  |   feeRange: number[]; // 2nd, 10th, 25th, 50th, 75th, 90th, 98th percentiles
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface CpfpSummary { | ||||||
|  |   transactions: TransactionExtended[]; | ||||||
|  |   clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export interface Statistic { | export interface Statistic { | ||||||
|   id?: number; |   id?: number; | ||||||
|   added: string; |   added: string; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { BlockExtended, BlockExtension, BlockPrice } from '../mempool.interfaces'; | import { BlockExtended, BlockExtension, BlockPrice, EffectiveFeeStats } from '../mempool.interfaces'; | ||||||
| import DB from '../database'; | import DB from '../database'; | ||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
| import { Common } from '../api/common'; | import { Common } from '../api/common'; | ||||||
| @ -908,6 +908,25 @@ class BlocksRepository { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Save indexed effective fee statistics | ||||||
|  |    *  | ||||||
|  |    * @param id  | ||||||
|  |    * @param feeStats  | ||||||
|  |    */ | ||||||
|  |   public async $saveEffectiveFeeStats(id: string, feeStats: EffectiveFeeStats): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       await DB.query(` | ||||||
|  |         UPDATE blocks SET median_fee = ?, fee_span = ? | ||||||
|  |         WHERE hash = ?`,
 | ||||||
|  |         [feeStats.medianFee, JSON.stringify(feeStats.feeRange), id] | ||||||
|  |       ); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`Cannot update block fee stats. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Convert a mysql row block into a BlockExtended. Note that you |    * Convert a mysql row block into a BlockExtended. Note that you | ||||||
|    * must provide the correct field into dbBlk object param |    * must provide the correct field into dbBlk object param | ||||||
|  | |||||||
| @ -48,7 +48,7 @@ class CpfpRepository { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $batchSaveClusters(clusters: { root: string, height: number, txs: any, effectiveFeePerVsize: number}[]): Promise<boolean> { |   public async $batchSaveClusters(clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]): Promise<boolean> { | ||||||
|     try { |     try { | ||||||
|       const clusterValues: any[] = []; |       const clusterValues: any[] = []; | ||||||
|       const txs: any[] = []; |       const txs: any[] = []; | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ | |||||||
|           </ng-template> |           </ng-template> | ||||||
|           <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span" |           <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span" | ||||||
|             *ngIf="block?.extras?.feeRange; else emptyfeespan"> |             *ngIf="block?.extras?.feeRange; else emptyfeespan"> | ||||||
|             {{ block?.extras?.feeRange?.[1] | number:feeRounding }} - {{ |             {{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{ | ||||||
|             block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container |             block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container | ||||||
|               i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> |               i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> | ||||||
|           </div> |           </div> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user