2024-04-06 02:24:22 +00:00
|
|
|
import { Ancestor, CpfpCluster, CpfpInfo, CpfpSummary, MempoolTransactionExtended, TransactionExtended } from '../mempool.interfaces';
|
|
|
|
import { GraphTx, convertToGraphTx, expandRelativesGraph, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from './mini-miner';
|
2024-01-08 00:56:48 +00:00
|
|
|
import memPool from './mempool';
|
2024-04-06 09:05:27 +00:00
|
|
|
import { Acceleration } from './acceleration/acceleration';
|
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
|
|
|
}
|
|
|
|
|
2024-04-06 02:24:22 +00:00
|
|
|
export function calculateGoodBlockCpfp(height: number, transactions: MempoolTransactionExtended[], accelerations: Acceleration[]): CpfpSummary {
|
|
|
|
const txMap: { [txid: string]: MempoolTransactionExtended } = {};
|
|
|
|
for (const tx of transactions) {
|
|
|
|
txMap[tx.txid] = tx;
|
|
|
|
}
|
|
|
|
const template = makeBlockTemplate(transactions, accelerations, 1, Infinity, Infinity);
|
|
|
|
const clusters = new Map<string, string[]>();
|
|
|
|
for (const tx of template) {
|
|
|
|
const cluster = tx.cluster || [];
|
|
|
|
const root = cluster.length ? cluster[cluster.length - 1] : null;
|
|
|
|
if (cluster.length > 1 && root && !clusters.has(root)) {
|
|
|
|
clusters.set(root, cluster);
|
|
|
|
}
|
|
|
|
txMap[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
|
|
|
|
}
|
|
|
|
|
|
|
|
const clusterArray: CpfpCluster[] = [];
|
|
|
|
|
|
|
|
for (const cluster of clusters.values()) {
|
|
|
|
for (const txid of cluster) {
|
|
|
|
const mempoolTx = txMap[txid];
|
|
|
|
if (mempoolTx) {
|
|
|
|
const ancestors: Ancestor[] = [];
|
|
|
|
const descendants: Ancestor[] = [];
|
|
|
|
let matched = false;
|
|
|
|
cluster.forEach(relativeTxid => {
|
|
|
|
if (relativeTxid === txid) {
|
|
|
|
matched = true;
|
|
|
|
} else {
|
|
|
|
const relative = {
|
|
|
|
txid: relativeTxid,
|
|
|
|
fee: txMap[relativeTxid].fee,
|
|
|
|
weight: (txMap[relativeTxid].adjustedVsize * 4) || txMap[relativeTxid].weight,
|
|
|
|
};
|
|
|
|
if (matched) {
|
|
|
|
descendants.push(relative);
|
|
|
|
} else {
|
|
|
|
ancestors.push(relative);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
|
|
|
mempoolTx.cpfpDirty = true;
|
|
|
|
}
|
|
|
|
Object.assign(mempoolTx, { ancestors, descendants, bestDescendant: null, cpfpChecked: true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const root = cluster[cluster.length - 1];
|
|
|
|
clusterArray.push({
|
|
|
|
root: root,
|
|
|
|
height,
|
|
|
|
txs: cluster.reverse().map(txid => ({
|
|
|
|
txid,
|
|
|
|
fee: txMap[txid].fee,
|
|
|
|
weight: (txMap[txid].adjustedVsize * 4) || txMap[txid].weight,
|
|
|
|
})),
|
|
|
|
effectiveFeePerVsize: txMap[root].effectiveFeePerVsize,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
transactions: transactions.map(tx => txMap[tx.txid]),
|
|
|
|
clusters: clusterArray,
|
|
|
|
};
|
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,
|
2024-07-18 16:44:00 +08:00
|
|
|
fee: tx.fee,
|
2024-01-05 22:25:07 +00:00
|
|
|
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,
|
2024-07-18 16:44:00 +08:00
|
|
|
fee: tx.fee,
|
2024-01-05 22:25:07 +00:00
|
|
|
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;
|
|
|
|
}
|