fix: resolve bug in fee rate range and filtering logic

This commit is contained in:
jramos0 2026-02-18 00:30:24 -06:00
parent 11475e30e2
commit f4cf989a66
No known key found for this signature in database
GPG key ID: DCC23B8412F4D2BB
2 changed files with 40 additions and 4 deletions

View file

@ -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
});
});
});

View file

@ -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))];
}