2024-04-04 04:28:57 +00:00
|
|
|
import { CpfpCluster, CpfpInfo, CpfpSummary, MempoolTransactionExtended, TransactionExtended } from '../mempool.interfaces';
|
|
|
|
import { GraphTx, convertToGraphTx, expandRelativesGraph, initializeRelatives, mempoolComparator, removeAncestors, setAncestorScores } from './mini-miner';
|
2024-01-08 00:56:48 +00:00
|
|
|
import memPool from './mempool';
|
2024-01-05 22:25:07 +00:00
|
|
|
|
|
|
|
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
|
2024-04-04 04:28:57 +00:00
|
|
|
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,
|
2024-01-05 22:25:07 +00:00
|
|
|
};
|
2024-04-04 04:28:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function calculateGoodBlockCpfp(height: number, transactions: TransactionExtended[]): CpfpSummary {
|
|
|
|
return calculateFastBlockCpfp(height, transactions, true);
|
2024-01-05 22:25:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
*/
|
2024-04-04 04:28:57 +00:00
|
|
|
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
|
2024-01-05 22:25:07 +00:00
|
|
|
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<string, GraphTx>();
|
2024-04-04 04:28:57 +00:00
|
|
|
const graphTx = convertToGraphTx(tx, memPool.getSpendMap());
|
2024-01-05 22:25:07 +00:00
|
|
|
ancestorMap.set(tx.txid, graphTx);
|
|
|
|
|
2024-04-04 04:28:57 +00:00
|
|
|
const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap());
|
2024-01-05 22:25:07 +00:00
|
|
|
const relativesMap = initializeRelatives(allRelatives);
|
|
|
|
const cluster = calculateCpfpCluster(tx.txid, relativesMap);
|
|
|
|
|
|
|
|
let totalVsize = 0;
|
|
|
|
let totalFee = 0;
|
|
|
|
for (const tx of cluster.values()) {
|
2024-04-04 04:28:57 +00:00
|
|
|
totalVsize += tx.vsize;
|
|
|
|
totalFee += tx.fees.base;
|
2024-01-05 22:25:07 +00:00
|
|
|
}
|
|
|
|
const effectiveFeePerVsize = totalFee / totalVsize;
|
|
|
|
for (const tx of cluster.values()) {
|
|
|
|
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
2024-04-04 04:28:57 +00:00
|
|
|
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 }));
|
2024-01-05 22:25:07 +00:00
|
|
|
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<string, GraphTx>): Map<string, GraphTx> {
|
|
|
|
const tx = graph.get(txid);
|
|
|
|
if (!tx) {
|
|
|
|
return new Map<string, GraphTx>([]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2024-04-04 04:28:57 +00:00
|
|
|
let maxIterations = MAX_CLUSTER_ITERATIONS;
|
2024-01-05 22:25:07 +00:00
|
|
|
let best = sortedRelatives.shift();
|
2024-04-04 04:28:57 +00:00
|
|
|
let bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
|
|
|
|
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestors.has(tx.txid)) && maxIterations > 0) {
|
2024-01-05 22:25:07 +00:00
|
|
|
maxIterations--;
|
2024-01-08 00:56:48 +00:00
|
|
|
if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) {
|
2024-01-05 22:25:07 +00:00
|
|
|
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) {
|
2024-04-04 04:28:57 +00:00
|
|
|
bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
|
2024-01-05 22:25:07 +00:00
|
|
|
bestCluster.set(best?.txid, best);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-08 00:56:48 +00:00
|
|
|
bestCluster.set(tx.txid, tx);
|
|
|
|
|
2024-01-05 22:25:07 +00:00
|
|
|
return bestCluster;
|
|
|
|
}
|