mirror of
https://github.com/mempool/mempool.git
synced 2024-11-20 10:21:52 +01:00
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;
|
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
|
||||||
cluster.forEach(tx => {
|
const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters);
|
||||||
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 = {};
|
|
||||||
}
|
|
||||||
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…
Reference in New Issue
Block a user