import { CpfpCluster, CpfpInfo, CpfpSummary, MempoolTransactionExtended, TransactionExtended } from '../mempool.interfaces'; import { GraphTx, convertToGraphTx, expandRelativesGraph, initializeRelatives, mempoolComparator, removeAncestors, setAncestorScores } from './mini-miner'; import memPool from './mempool'; const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction const MAX_CLUSTER_ITERATIONS = 100; export function calculateFastBlockCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary { 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: { [txid: string]: TransactionExtended } = {}; // 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]; if (!ancestors[tx.txid]) { let totalFee = 0; let totalVSize = 0; clusterTxs.forEach(tx => { totalFee += tx?.fee || 0; totalVSize += (tx.weight / 4); }); const effectiveFeePerVsize = totalFee / totalVSize; let cluster: CpfpCluster; if (clusterTxs.length > 1) { cluster = { root: clusterTxs[0].txid, height, txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }), effectiveFeePerVsize, }; clusters.push(cluster); } clusterTxs.forEach(tx => { txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; if (cluster) { clusterMap[tx.txid] = cluster; } }); // reset working vars clusterTxs = []; ancestors = {}; } 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; } } } if (saveRelatives) { for (const cluster of clusters) { cluster.txs.forEach((member, index) => { txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse(); txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse(); txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize; }); } } return { transactions, clusters, }; } export function calculateGoodBlockCpfp(height: number, transactions: TransactionExtended[]): CpfpSummary { return calculateFastBlockCpfp(height, transactions, true); } /** * Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for * that transaction (and all others in the same cluster) */ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo { if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) { tx.cpfpDirty = false; return { ancestors: tx.ancestors || [], bestDescendant: tx.bestDescendant || null, descendants: tx.descendants || [], effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize, sigops: tx.sigops, adjustedVsize: tx.adjustedVsize, acceleration: tx.acceleration }; } const ancestorMap = new Map(); const graphTx = convertToGraphTx(tx, memPool.getSpendMap()); ancestorMap.set(tx.txid, graphTx); const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap()); const relativesMap = initializeRelatives(allRelatives); const cluster = calculateCpfpCluster(tx.txid, relativesMap); let totalVsize = 0; let totalFee = 0; for (const tx of cluster.values()) { totalVsize += tx.vsize; totalFee += tx.fees.base; } const effectiveFeePerVsize = totalFee / totalVsize; for (const tx of cluster.values()) { mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); mempool[tx.txid].bestDescendant = null; mempool[tx.txid].cpfpChecked = true; mempool[tx.txid].cpfpDirty = true; mempool[tx.txid].cpfpUpdated = Date.now(); } tx = mempool[tx.txid]; return { ancestors: tx.ancestors || [], bestDescendant: tx.bestDescendant || null, descendants: tx.descendants || [], effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize, sigops: tx.sigops, adjustedVsize: tx.adjustedVsize, acceleration: tx.acceleration }; } /** * Given a root transaction and a list of in-mempool ancestors, * Calculate the CPFP cluster * * @param tx * @param ancestors */ function calculateCpfpCluster(txid: string, graph: Map): Map { const tx = graph.get(txid); if (!tx) { return new Map([]); } // Initialize individual & ancestor fee rates graph.forEach(entry => setAncestorScores(entry)); // Sort by descending ancestor score let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator); // Iterate until we reach a cluster that includes our target tx let maxIterations = MAX_CLUSTER_ITERATIONS; let best = sortedRelatives.shift(); let bestCluster = new Map(best?.ancestors?.entries() || []); while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestors.has(tx.txid)) && maxIterations > 0) { maxIterations--; if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) { break; } else { // Remove this cluster (it doesn't include our target tx) // and update scores, ancestor totals and dependencies for the survivors removeAncestors(bestCluster, graph); // re-sort sortedRelatives = Array.from(graph.values()).sort(mempoolComparator); // Grab the next highest scoring entry best = sortedRelatives.shift(); if (best) { bestCluster = new Map(best?.ancestors?.entries() || []); bestCluster.set(best?.txid, best); } } } bestCluster.set(tx.txid, tx); return bestCluster; }