diff --git a/backend/src/__tests__/api/common.ts b/backend/src/__tests__/api/common.ts index 5380391ac..fb245e15a 100644 --- a/backend/src/__tests__/api/common.ts +++ b/backend/src/__tests__/api/common.ts @@ -37,4 +37,27 @@ describe('Common', () => { }); }); }); + + describe('Effective Fee Statistics', () => { + test('returns safe defaults for blocks with only coinbase', () => { + const coinbaseTx = { weight: 1000, fee: 0, txid: 'coinbase0' }; + const result = Common.calcEffectiveFeeStatistics([coinbaseTx]); + + expect(result.medianFee).toBe(0); + expect(result.feeRange).toEqual([0, 0, 0, 0, 0, 0, 0]); + }); + + test('excludes coinbase from fee stats when multiple txs', () => { + const coinbaseTx = { weight: 1000, fee: 0, txid: 'coinbase0' }; + const tx1 = { weight: 400, fee: 100, txid: 'tx1' }; // vsize 100, rate 1 sat/vB + const tx2 = { weight: 400, fee: 250, txid: 'tx2' }; // vsize 100, rate 2.5 sat/vB + + const result = Common.calcEffectiveFeeStatistics([coinbaseTx, tx1, tx2]); + + // Verify that coinbase (fee 0) was excluded from stats + // Fee range min/max should be > 0 (not affected by coinbase's 0 fee) + expect(result.feeRange[0]).toBeGreaterThan(0); // min fee + expect(result.feeRange[6]).toBeGreaterThan(0); // max fee + }); + }); }); diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 2538d7bd1..8fcd2b27b 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1030,8 +1030,18 @@ export class Common { } static calcEffectiveFeeStatistics(transactions: { weight: number, fee?: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): 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); - const totalWeight = transactions.reduce((acc, tx) => acc + tx.weight, 0); + // return early with safe default values + if (transactions.length <= 1) { + return { + medianFee: 0, + feeRange: [0, 0, 0, 0, 0, 0, 0], + }; + } + // assume the first transaction is a coinbase if the fee is falsy (0 or undefined) + const nonCoinbaseTransactions = transactions[0].fee ? transactions : transactions.slice(1); + + const sortedTxs = nonCoinbaseTransactions.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); + const totalWeight = nonCoinbaseTransactions.reduce((acc, tx) => acc + tx.weight, 0); // include any unused space let weightCount = config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight; @@ -1060,7 +1070,7 @@ export class Common { // 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) + nonCoinbaseTransactions.slice(Math.ceil(nonCoinbaseTransactions.length * 49 / 50)).reduce((min, tx) => { return Math.min(min, tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4))); }, Infinity) ); // maximum effective fee heuristic: @@ -1069,7 +1079,7 @@ export class Common { // 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) + nonCoinbaseTransactions.slice(0, nonCoinbaseTransactions.length / 50).reduce((max, tx) => { return Math.max(max, tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4))); }, 0) ); return { @@ -1083,6 +1093,9 @@ export class Common { } static getNthPercentile(n: number, sortedDistribution: any[]): any { + if (sortedDistribution.length === 0) { + return { rate: 0 }; + } return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))]; } diff --git a/contributors/jramos0.txt b/contributors/jramos0.txt new file mode 100644 index 000000000..6bca26801 --- /dev/null +++ b/contributors/jramos0.txt @@ -0,0 +1,3 @@ +I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of February 6, 2025. + +Signed: jramos0