diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 07a724f0c..234535cc4 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1,4 +1,4 @@ -import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces'; +import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces'; import config from '../config'; import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { isIP } from 'net'; @@ -374,40 +374,80 @@ 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 clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block + const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster + let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster + let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root const txMap = {}; + // initialize the txMap + for (const tx of transactions) { + txMap[tx.txid] = tx; + } + // reverse pass to identify CPFP clusters 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 => { + clusterTxs.forEach(tx => { totalFee += tx?.fee || 0; totalVSize += (tx.weight / 4); }); const effectiveFeePerVsize = totalFee / totalVSize; - if (cluster.length > 1) { - clusters.push({ - root: cluster[0].txid, + let cluster: CpfpCluster; + if (clusterTxs.length > 1) { + cluster = { + root: clusterTxs[0].txid, height, - txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }), + txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }), effectiveFeePerVsize, - }); + }; + clusters.push(cluster); } - cluster.forEach(tx => { + clusterTxs.forEach(tx => { txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; + if (cluster) { + clusterMap[tx.txid] = cluster; + } }); - cluster = []; + // reset working vars + clusterTxs = []; ancestors = {}; } - cluster.push(tx); + clusterTxs.push(tx); tx.vin.forEach(vin => { ancestors[vin.txid] = true; }); } + // forward pass to enforce ancestor rate caps + for (const tx of transactions) { + let minAncestorRate = tx.effectiveFeePerVsize; + for (const vin of tx.vin) { + if (txMap[vin.txid]?.effectiveFeePerVsize) { + minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize); + } + } + // check rounded values to skip cases with almost identical fees + const roundedMinAncestorRate = Math.ceil(minAncestorRate); + const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize); + if (roundedMinAncestorRate < roundedEffectiveFeeRate) { + tx.effectiveFeePerVsize = minAncestorRate; + if (!clusterMap[tx.txid]) { + // add a single-tx cluster to record the dependent rate + const cluster = { + root: tx.txid, + height, + txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }], + effectiveFeePerVsize: minAncestorRate, + }; + clusterMap[tx.txid] = cluster; + clusters.push(cluster); + } else { + // update the existing cluster with the dependent rate + clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate; + } + } + } return { transactions, clusters, diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index c3e0d02ba..1ed8de8a3 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -253,9 +253,16 @@ export interface WorkingEffectiveFeeStats extends EffectiveFeeStats { maxFee: number; } +export interface CpfpCluster { + root: string, + height: number, + txs: Ancestor[], + effectiveFeePerVsize: number, +} + export interface CpfpSummary { transactions: TransactionExtended[]; - clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]; + clusters: CpfpCluster[]; } export interface Statistic { diff --git a/backend/src/repositories/CpfpRepository.ts b/backend/src/repositories/CpfpRepository.ts index b68c25472..e712e6009 100644 --- a/backend/src/repositories/CpfpRepository.ts +++ b/backend/src/repositories/CpfpRepository.ts @@ -1,8 +1,7 @@ -import cluster, { Cluster } from 'cluster'; import { RowDataPacket } from 'mysql2'; import DB from '../database'; import logger from '../logger'; -import { Ancestor } from '../mempool.interfaces'; +import { Ancestor, CpfpCluster } from '../mempool.interfaces'; import transactionRepository from '../repositories/TransactionRepository'; class CpfpRepository { @@ -12,7 +11,7 @@ class CpfpRepository { } // skip clusters of transactions with the same fees const roundedEffectiveFee = Math.round(effectiveFeePerVsize * 100) / 100; - const equalFee = txs.reduce((acc, tx) => { + const equalFee = txs.length > 1 && txs.reduce((acc, tx) => { return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee); }, true); if (equalFee) { @@ -54,9 +53,9 @@ class CpfpRepository { const txs: any[] = []; for (const cluster of clusters) { - if (cluster.txs?.length > 1) { + if (cluster.txs?.length) { const roundedEffectiveFee = Math.round(cluster.effectiveFeePerVsize * 100) / 100; - const equalFee = cluster.txs.reduce((acc, tx) => { + const equalFee = cluster.txs.length > 1 && cluster.txs.reduce((acc, tx) => { return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee); }, true); if (!equalFee) { @@ -111,7 +110,7 @@ class CpfpRepository { } } - public async $getCluster(clusterRoot: string): Promise { + public async $getCluster(clusterRoot: string): Promise { const [clusterRows]: any = await DB.query( ` SELECT * @@ -121,6 +120,7 @@ class CpfpRepository { [clusterRoot] ); const cluster = clusterRows[0]; + cluster.effectiveFeePerVsize = cluster.fee_rate; if (cluster?.txs) { cluster.txs = this.unpack(cluster.txs); return cluster; diff --git a/backend/src/repositories/TransactionRepository.ts b/backend/src/repositories/TransactionRepository.ts index 279a2bdad..bde95df9b 100644 --- a/backend/src/repositories/TransactionRepository.ts +++ b/backend/src/repositories/TransactionRepository.ts @@ -105,6 +105,7 @@ class TransactionRepository { return { descendants, ancestors, + effectiveFeePerVsize: cluster.effectiveFeePerVsize, }; } }