Merge pull request #4905 from mempool/mononaut/mini-miner-cpfp
Mini miner cpfp
This commit is contained in:
		
						commit
						b0fac806d0
					
				| @ -1,15 +1,14 @@ | |||||||
| import logger from '../../logger'; | import logger from '../../logger'; | ||||||
| import { MempoolTransactionExtended } from '../../mempool.interfaces'; | import { MempoolTransactionExtended } from '../../mempool.interfaces'; | ||||||
| import { IEsploraApi } from '../bitcoin/esplora-api.interface'; | import { GraphTx, getSameBlockRelatives, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from '../mini-miner'; | ||||||
| 
 | 
 | ||||||
| const BLOCK_WEIGHT_UNITS = 4_000_000; | const BLOCK_WEIGHT_UNITS = 4_000_000; | ||||||
| const BLOCK_SIGOPS = 80_000; |  | ||||||
| const MAX_RELATIVE_GRAPH_SIZE = 200; | const MAX_RELATIVE_GRAPH_SIZE = 200; | ||||||
| const BID_BOOST_WINDOW = 40_000; | const BID_BOOST_WINDOW = 40_000; | ||||||
| const BID_BOOST_MIN_OFFSET = 10_000; | const BID_BOOST_MIN_OFFSET = 10_000; | ||||||
| const BID_BOOST_MAX_OFFSET = 400_000; | const BID_BOOST_MAX_OFFSET = 400_000; | ||||||
| 
 | 
 | ||||||
| type Acceleration = { | export type Acceleration = { | ||||||
|   txid: string; |   txid: string; | ||||||
|   max_bid: number; |   max_bid: number; | ||||||
| }; | }; | ||||||
| @ -28,31 +27,6 @@ export interface AccelerationInfo { | |||||||
|   cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
 |   cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface GraphTx { |  | ||||||
|   txid: string; |  | ||||||
|   vsize: number; |  | ||||||
|   weight: number; |  | ||||||
|   fees: { |  | ||||||
|     base: number; // in sats
 |  | ||||||
|   }; |  | ||||||
|   depends: string[]; |  | ||||||
|   spentby: string[]; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface MempoolTx extends GraphTx { |  | ||||||
|   ancestorcount: number; |  | ||||||
|   ancestorsize: number; |  | ||||||
|   fees: { // in sats
 |  | ||||||
|     base: number; |  | ||||||
|     ancestor: number; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   ancestors: Map<string, MempoolTx>, |  | ||||||
|   ancestorRate: number; |  | ||||||
|   individualRate: number; |  | ||||||
|   score: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class AccelerationCosts { | class AccelerationCosts { | ||||||
|   /** |   /** | ||||||
|    * Takes a list of accelerations and verbose block data |    * Takes a list of accelerations and verbose block data | ||||||
| @ -61,7 +35,7 @@ class AccelerationCosts { | |||||||
|    * @param accelerationsx |    * @param accelerationsx | ||||||
|    * @param verboseBlock |    * @param verboseBlock | ||||||
|    */ |    */ | ||||||
|   public calculateBoostRate(accelerations: Acceleration[], blockTxs: IEsploraApi.Transaction[]): number { |   public calculateBoostRate(accelerations: Acceleration[], blockTxs: MempoolTransactionExtended[]): number { | ||||||
|     // Run GBT ourselves to calculate accurate effective fee rates
 |     // Run GBT ourselves to calculate accurate effective fee rates
 | ||||||
|     // the list of transactions comes from a mined block, so we already know everything fits within consensus limits
 |     // the list of transactions comes from a mined block, so we already know everything fits within consensus limits
 | ||||||
|     const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity); |     const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity); | ||||||
| @ -170,108 +144,28 @@ class AccelerationCosts { | |||||||
|   /** |   /** | ||||||
|    * Takes an accelerated mined txid and a target rate |    * Takes an accelerated mined txid and a target rate | ||||||
|    * Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors |    * Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors | ||||||
|    *  |    * | ||||||
|    * @param txid  |    * @param txid | ||||||
|    * @param medianFeeRate  |    * @param medianFeeRate | ||||||
|    */ |    */ | ||||||
|   public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo { |   public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo { | ||||||
|     // Get same-block transaction ancestors
 |     // Get same-block transaction ancestors
 | ||||||
|     const allRelatives = this.getSameBlockRelatives(tx, transactions); |     const allRelatives = getSameBlockRelatives(tx, transactions); | ||||||
|     const relativesMap = this.initializeRelatives(allRelatives); |     const relativesMap = initializeRelatives(allRelatives); | ||||||
|     const rootTx = relativesMap.get(tx.txid) as MempoolTx; |     const rootTx = relativesMap.get(tx.txid) as GraphTx; | ||||||
| 
 | 
 | ||||||
|     // Calculate cost to boost
 |     // Calculate cost to boost
 | ||||||
|     return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate); |     return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |  | ||||||
|    * Takes a raw transaction, and builds a graph of same-block relatives, |  | ||||||
|    * and returns as a MempoolTx |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    */ |  | ||||||
|   private getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> { |  | ||||||
|     const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
 |  | ||||||
|     const spendMap = new Map<string, string>(); // map of outpoints to spending txids
 |  | ||||||
|     for (const tx of transactions) { |  | ||||||
|       blockTxs.set(tx.txid, tx); |  | ||||||
|       for (const vin of tx.vin) { |  | ||||||
|         spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const relatives: Map<string, GraphTx> = new Map(); |  | ||||||
|     const stack: string[] = [tx.txid]; |  | ||||||
| 
 |  | ||||||
|     // build set of same-block ancestors
 |  | ||||||
|     while (stack.length > 0) { |  | ||||||
|       const nextTxid = stack.pop(); |  | ||||||
|       const nextTx = nextTxid ? blockTxs.get(nextTxid) : null; |  | ||||||
|       if (!nextTx || relatives.has(nextTx.txid)) { |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       const mempoolTx = this.convertToGraphTx(nextTx); |  | ||||||
| 
 |  | ||||||
|       mempoolTx.fees.base = nextTx.fee || 0; |  | ||||||
|       mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[]; |  | ||||||
|       mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[]; |  | ||||||
| 
 |  | ||||||
|       for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) { |  | ||||||
|         if (txid) { |  | ||||||
|           stack.push(txid); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       relatives.set(mempoolTx.txid, mempoolTx); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return relatives; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Takes a raw transaction and converts it to MempoolTx format |  | ||||||
|    * fee and ancestor data is initialized with dummy/null values |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    */ |  | ||||||
|   private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx { |  | ||||||
|     return { |  | ||||||
|       txid: tx.txid, |  | ||||||
|       vsize: Math.ceil(tx.weight / 4), |  | ||||||
|       weight: tx.weight, |  | ||||||
|       fees: { |  | ||||||
|         base: 0, // dummy
 |  | ||||||
|       }, |  | ||||||
|       depends: [], // dummy
 |  | ||||||
|       spentby: [], //dummy
 |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private convertGraphToMempoolTx(tx: GraphTx): MempoolTx { |  | ||||||
|     return { |  | ||||||
|       ...tx, |  | ||||||
|       fees: { |  | ||||||
|         base: tx.fees.base, |  | ||||||
|         ancestor: tx.fees.base, |  | ||||||
|       }, |  | ||||||
|       ancestorcount: 1, |  | ||||||
|       ancestorsize: Math.ceil(tx.weight / 4), |  | ||||||
|       ancestors: new Map<string, MempoolTx>(), |  | ||||||
|       ancestorRate: 0, |  | ||||||
|       individualRate: 0, |  | ||||||
|       score: 0, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |   /** | ||||||
|    * Given a root transaction, a list of in-mempool ancestors, and a target fee rate, |    * Given a root transaction, a list of in-mempool ancestors, and a target fee rate, | ||||||
|    * Calculate the minimum set of transactions to fee-bump, their total vsize + fees |    * Calculate the minimum set of transactions to fee-bump, their total vsize + fees | ||||||
|    *  |    * | ||||||
|    * @param tx |    * @param tx | ||||||
|    * @param ancestors |    * @param ancestors | ||||||
|    */ |    */ | ||||||
|   private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map<string, MempoolTx>, targetFeeRate: number): AccelerationInfo { |   private calculateAccelerationAncestors(tx: GraphTx, relatives: Map<string, GraphTx>, targetFeeRate: number): AccelerationInfo { | ||||||
|     // add root tx to the ancestor map
 |     // add root tx to the ancestor map
 | ||||||
|     relatives.set(tx.txid, tx); |     relatives.set(tx.txid, tx); | ||||||
| 
 | 
 | ||||||
| @ -283,12 +177,12 @@ class AccelerationCosts { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     // Initialize individual & ancestor fee rates
 |     // Initialize individual & ancestor fee rates
 | ||||||
|     relatives.forEach(entry => this.setAncestorScores(entry)); |     relatives.forEach(entry => setAncestorScores(entry)); | ||||||
| 
 | 
 | ||||||
|     // Sort by descending ancestor score
 |     // Sort by descending ancestor score
 | ||||||
|     let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator); |     let sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator); | ||||||
| 
 | 
 | ||||||
|     let includedInCluster: Map<string, MempoolTx> | null = null; |     let includedInCluster: Map<string, GraphTx> | null = null; | ||||||
| 
 | 
 | ||||||
|     // While highest score >= targetFeeRate
 |     // While highest score >= targetFeeRate
 | ||||||
|     let maxIterations = MAX_RELATIVE_GRAPH_SIZE; |     let maxIterations = MAX_RELATIVE_GRAPH_SIZE; | ||||||
| @ -297,17 +191,17 @@ class AccelerationCosts { | |||||||
|       // Grab the highest scoring entry
 |       // Grab the highest scoring entry
 | ||||||
|       const best = sortedRelatives.shift(); |       const best = sortedRelatives.shift(); | ||||||
|       if (best) { |       if (best) { | ||||||
|         const cluster = new Map<string, MempoolTx>(best.ancestors?.entries() || []); |         const cluster = new Map<string, GraphTx>(best.ancestors?.entries() || []); | ||||||
|         if (best.ancestors.has(tx.txid)) { |         if (best.ancestors.has(tx.txid)) { | ||||||
|           includedInCluster = cluster; |           includedInCluster = cluster; | ||||||
|         } |         } | ||||||
|         cluster.set(best.txid, best); |         cluster.set(best.txid, best); | ||||||
|         // Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
 |         // Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
 | ||||||
|         // and update scores, ancestor totals and dependencies for the survivors
 |         // and update scores, ancestor totals and dependencies for the survivors
 | ||||||
|         this.removeAncestors(cluster, relatives); |         removeAncestors(cluster, relatives); | ||||||
| 
 | 
 | ||||||
|         // re-sort
 |         // re-sort
 | ||||||
|         sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator); |         sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -345,394 +239,6 @@ class AccelerationCosts { | |||||||
|       nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate), |       nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate), | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors |  | ||||||
|    * for each transaction. |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    * @param all  |  | ||||||
|    */ |  | ||||||
|   private setAncestors(tx: MempoolTx, all: Map<string, MempoolTx>, visited: Map<string, Map<string, MempoolTx>>, depth: number = 0): Map<string, MempoolTx> { |  | ||||||
|     // sanity check for infinite recursion / too many ancestors (should never happen)
 |  | ||||||
|     if (depth >= 100) { |  | ||||||
|       logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`); |  | ||||||
|       throw new Error('invalid_tx_dependencies'); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // initialize the ancestor map for this tx
 |  | ||||||
|     tx.ancestors = new Map<string, MempoolTx>(); |  | ||||||
|     tx.depends.forEach(parentId => { |  | ||||||
|       const parent = all.get(parentId); |  | ||||||
|       if (parent) { |  | ||||||
|         // add the parent
 |  | ||||||
|         tx.ancestors?.set(parentId, parent); |  | ||||||
|         // check for a cached copy of this parent's ancestors
 |  | ||||||
|         let ancestors = visited.get(parent.txid); |  | ||||||
|         if (!ancestors) { |  | ||||||
|           // recursively fetch the parent's ancestors
 |  | ||||||
|           ancestors = this.setAncestors(parent, all, visited, depth + 1); |  | ||||||
|         } |  | ||||||
|         // and add to this tx's map
 |  | ||||||
|         ancestors.forEach((ancestor, ancestorId) => { |  | ||||||
|           tx.ancestors?.set(ancestorId, ancestor); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     visited.set(tx.txid, tx.ancestors); |  | ||||||
| 
 |  | ||||||
|     return tx.ancestors; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph |  | ||||||
|    * by running setAncestors on each leaf, and caching intermediate results. |  | ||||||
|    * then initializes ancestor data for each transaction |  | ||||||
|    *  |  | ||||||
|    * @param all  |  | ||||||
|    */ |  | ||||||
|   private initializeRelatives(all: Map<string, GraphTx>): Map<string, MempoolTx> { |  | ||||||
|     const mempoolTxs = new Map<string, MempoolTx>(); |  | ||||||
|     all.forEach(entry => { |  | ||||||
|       mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry)); |  | ||||||
|     }); |  | ||||||
|     const visited: Map<string, Map<string, MempoolTx>> = new Map(); |  | ||||||
|     const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0); |  | ||||||
|     for (const leaf of leaves) { |  | ||||||
|       this.setAncestors(leaf, mempoolTxs, visited); |  | ||||||
|     } |  | ||||||
|     mempoolTxs.forEach(entry => { |  | ||||||
|       entry.ancestors?.forEach(ancestor => { |  | ||||||
|         entry.ancestorcount++; |  | ||||||
|         entry.ancestorsize += ancestor.vsize; |  | ||||||
|         entry.fees.ancestor += ancestor.fees.base; |  | ||||||
|       }); |  | ||||||
|       this.setAncestorScores(entry); |  | ||||||
|     }); |  | ||||||
|     return mempoolTxs; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Remove a cluster of transactions from an in-mempool dependency graph |  | ||||||
|    * and update the survivors' scores and ancestors |  | ||||||
|    *  |  | ||||||
|    * @param cluster  |  | ||||||
|    * @param ancestors  |  | ||||||
|    */ |  | ||||||
|   private removeAncestors(cluster: Map<string, MempoolTx>, all: Map<string, MempoolTx>): void { |  | ||||||
|     // remove
 |  | ||||||
|     cluster.forEach(tx => { |  | ||||||
|       all.delete(tx.txid); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // update survivors
 |  | ||||||
|     all.forEach(tx => { |  | ||||||
|       cluster.forEach(remove => { |  | ||||||
|         if (tx.ancestors?.has(remove.txid)) { |  | ||||||
|           // remove as dependency
 |  | ||||||
|           tx.ancestors.delete(remove.txid); |  | ||||||
|           tx.depends = tx.depends.filter(parent => parent !== remove.txid); |  | ||||||
|           // update ancestor sizes and fees
 |  | ||||||
|           tx.ancestorsize -= remove.vsize; |  | ||||||
|           tx.fees.ancestor -= remove.fees.base; |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|       // recalculate fee rates
 |  | ||||||
|       this.setAncestorScores(tx); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Take a mempool transaction, and set the fee rates and ancestor score |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    */ |  | ||||||
|   private setAncestorScores(tx: MempoolTx): void { |  | ||||||
|     tx.individualRate = tx.fees.base / tx.vsize; |  | ||||||
|     tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize; |  | ||||||
|     tx.score = Math.min(tx.individualRate, tx.ancestorRate); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Sort by descending score
 |  | ||||||
|   private mempoolComparator(a, b): number { |  | ||||||
|     return b.score - a.score; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new AccelerationCosts; | export default new AccelerationCosts; | ||||||
| 
 |  | ||||||
| interface TemplateTransaction { |  | ||||||
|   txid: string; |  | ||||||
|   order: number; |  | ||||||
|   weight: number; |  | ||||||
|   adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
 |  | ||||||
|   sigops: number; |  | ||||||
|   fee: number; |  | ||||||
|   feeDelta: number; |  | ||||||
|   ancestors: string[]; |  | ||||||
|   cluster: string[]; |  | ||||||
|   effectiveFeePerVsize: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface MinerTransaction extends TemplateTransaction { |  | ||||||
|   inputs: string[]; |  | ||||||
|   feePerVsize: number; |  | ||||||
|   relativesSet: boolean; |  | ||||||
|   ancestorMap: Map<string, MinerTransaction>; |  | ||||||
|   children: Set<MinerTransaction>; |  | ||||||
|   ancestorFee: number; |  | ||||||
|   ancestorVsize: number; |  | ||||||
|   ancestorSigops: number; |  | ||||||
|   score: number; |  | ||||||
|   used: boolean; |  | ||||||
|   modified: boolean; |  | ||||||
|   dependencyRate: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /* |  | ||||||
| * Build a block using an approximation of the transaction selection algorithm from Bitcoin Core |  | ||||||
| * (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
 |  | ||||||
| */ |  | ||||||
| export function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] { |  | ||||||
|   const auditPool: Map<string, MinerTransaction> = new Map(); |  | ||||||
|   const mempoolArray: MinerTransaction[] = []; |  | ||||||
|    |  | ||||||
|   candidates.forEach(tx => { |  | ||||||
|     // initializing everything up front helps V8 optimize property access later
 |  | ||||||
|     const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0))); |  | ||||||
|     const feePerVsize = (tx.fee / adjustedVsize); |  | ||||||
|     auditPool.set(tx.txid, { |  | ||||||
|       txid: tx.txid, |  | ||||||
|       order: txidToOrdering(tx.txid), |  | ||||||
|       fee: tx.fee, |  | ||||||
|       feeDelta: 0, |  | ||||||
|       weight: tx.weight, |  | ||||||
|       adjustedVsize, |  | ||||||
|       feePerVsize: feePerVsize, |  | ||||||
|       effectiveFeePerVsize: feePerVsize, |  | ||||||
|       dependencyRate: feePerVsize, |  | ||||||
|       sigops: tx.sigops || 0, |  | ||||||
|       inputs: (tx.vin?.map(vin => vin.txid) || []) as string[], |  | ||||||
|       relativesSet: false, |  | ||||||
|       ancestors: [], |  | ||||||
|       cluster: [], |  | ||||||
|       ancestorMap: new Map<string, MinerTransaction>(), |  | ||||||
|       children: new Set<MinerTransaction>(), |  | ||||||
|       ancestorFee: 0, |  | ||||||
|       ancestorVsize: 0, |  | ||||||
|       ancestorSigops: 0, |  | ||||||
|       score: 0, |  | ||||||
|       used: false, |  | ||||||
|       modified: false, |  | ||||||
|     }); |  | ||||||
|     mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   // set accelerated effective fee
 |  | ||||||
|   for (const acceleration of accelerations) { |  | ||||||
|     const tx = auditPool.get(acceleration.txid); |  | ||||||
|     if (tx) { |  | ||||||
|       tx.feeDelta = acceleration.max_bid; |  | ||||||
|       tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize); |  | ||||||
|       tx.effectiveFeePerVsize = tx.feePerVsize; |  | ||||||
|       tx.dependencyRate = tx.feePerVsize; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Build relatives graph & calculate ancestor scores
 |  | ||||||
|   for (const tx of mempoolArray) { |  | ||||||
|     if (!tx.relativesSet) { |  | ||||||
|       setRelatives(tx, auditPool); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Sort by descending ancestor score
 |  | ||||||
|   mempoolArray.sort(priorityComparator); |  | ||||||
| 
 |  | ||||||
|   // Build blocks by greedily choosing the highest feerate package
 |  | ||||||
|   // (i.e. the package rooted in the transaction with the best ancestor score)
 |  | ||||||
|   const blocks: number[][] = []; |  | ||||||
|   let blockWeight = 0; |  | ||||||
|   let blockSigops = 0; |  | ||||||
|   const transactions: MinerTransaction[] = []; |  | ||||||
|   let modified: MinerTransaction[] = []; |  | ||||||
|   const overflow: MinerTransaction[] = []; |  | ||||||
|   let failures = 0; |  | ||||||
|   while (mempoolArray.length || modified.length) { |  | ||||||
|     // skip invalid transactions
 |  | ||||||
|     while (mempoolArray[0].used || mempoolArray[0].modified) { |  | ||||||
|       mempoolArray.shift(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Select best next package
 |  | ||||||
|     let nextTx; |  | ||||||
|     const nextPoolTx = mempoolArray[0]; |  | ||||||
|     const nextModifiedTx = modified[0]; |  | ||||||
|     if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) { |  | ||||||
|       nextTx = nextPoolTx; |  | ||||||
|       mempoolArray.shift(); |  | ||||||
|     } else { |  | ||||||
|       modified.shift(); |  | ||||||
|       if (nextModifiedTx) { |  | ||||||
|         nextTx = nextModifiedTx; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (nextTx && !nextTx?.used) { |  | ||||||
|       // Check if the package fits into this block
 |  | ||||||
|       if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) { |  | ||||||
|         const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values()); |  | ||||||
|         // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
 |  | ||||||
|         const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx]; |  | ||||||
|         const clusterTxids = sortedTxSet.map(tx => tx.txid); |  | ||||||
|         const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize); |  | ||||||
|         const used: MinerTransaction[] = []; |  | ||||||
|         while (sortedTxSet.length) { |  | ||||||
|           const ancestor = sortedTxSet.pop(); |  | ||||||
|           if (!ancestor) { |  | ||||||
|             continue; |  | ||||||
|           } |  | ||||||
|           ancestor.used = true; |  | ||||||
|           ancestor.usedBy = nextTx.txid; |  | ||||||
|           // update this tx with effective fee rate & relatives data
 |  | ||||||
|           if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) { |  | ||||||
|             ancestor.effectiveFeePerVsize = effectiveFeeRate; |  | ||||||
|           } |  | ||||||
|           ancestor.cluster = clusterTxids; |  | ||||||
|           transactions.push(ancestor); |  | ||||||
|           blockWeight += ancestor.weight; |  | ||||||
|           blockSigops += ancestor.sigops; |  | ||||||
|           used.push(ancestor); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // remove these as valid package ancestors for any descendants remaining in the mempool
 |  | ||||||
|         if (used.length) { |  | ||||||
|           used.forEach(tx => { |  | ||||||
|             modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate); |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         failures = 0; |  | ||||||
|       } else { |  | ||||||
|         // hold this package in an overflow list while we check for smaller options
 |  | ||||||
|         overflow.push(nextTx); |  | ||||||
|         failures++; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // this block is full
 |  | ||||||
|     const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000); |  | ||||||
|     const queueEmpty = !mempoolArray.length && !modified.length; |  | ||||||
| 
 |  | ||||||
|     if (exceededPackageTries || queueEmpty) { |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   for (const tx of transactions) { |  | ||||||
|     tx.ancestors = Object.values(tx.ancestorMap); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return transactions; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // traverse in-mempool ancestors
 |  | ||||||
| // recursion unavoidable, but should be limited to depth < 25 by mempool policy
 |  | ||||||
| function setRelatives( |  | ||||||
|   tx: MinerTransaction, |  | ||||||
|   mempool: Map<string, MinerTransaction>, |  | ||||||
| ): void { |  | ||||||
|   for (const parent of tx.inputs) { |  | ||||||
|     const parentTx = mempool.get(parent); |  | ||||||
|     if (parentTx && !tx.ancestorMap?.has(parent)) { |  | ||||||
|       tx.ancestorMap.set(parent, parentTx); |  | ||||||
|       parentTx.children.add(tx); |  | ||||||
|       // visit each node only once
 |  | ||||||
|       if (!parentTx.relativesSet) { |  | ||||||
|         setRelatives(parentTx, mempool); |  | ||||||
|       } |  | ||||||
|       parentTx.ancestorMap.forEach((ancestor) => { |  | ||||||
|         tx.ancestorMap.set(ancestor.txid, ancestor); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|   tx.ancestorFee = (tx.fee + tx.feeDelta); |  | ||||||
|   tx.ancestorVsize = tx.adjustedVsize || 0; |  | ||||||
|   tx.ancestorSigops = tx.sigops || 0; |  | ||||||
|   tx.ancestorMap.forEach((ancestor) => { |  | ||||||
|     tx.ancestorFee += (ancestor.fee + ancestor.feeDelta); |  | ||||||
|     tx.ancestorVsize += ancestor.adjustedVsize; |  | ||||||
|     tx.ancestorSigops += ancestor.sigops; |  | ||||||
|   }); |  | ||||||
|   tx.score = tx.ancestorFee / tx.ancestorVsize; |  | ||||||
|   tx.relativesSet = true; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
 |  | ||||||
| // avoids recursion to limit call stack depth
 |  | ||||||
| function updateDescendants( |  | ||||||
|   rootTx: MinerTransaction, |  | ||||||
|   mempool: Map<string, MinerTransaction>, |  | ||||||
|   modified: MinerTransaction[], |  | ||||||
|   clusterRate: number, |  | ||||||
| ): MinerTransaction[] { |  | ||||||
|   const descendantSet: Set<MinerTransaction> = new Set(); |  | ||||||
|   // stack of nodes left to visit
 |  | ||||||
|   const descendants: MinerTransaction[] = []; |  | ||||||
|   let descendantTx: MinerTransaction | undefined; |  | ||||||
|   rootTx.children.forEach(childTx => { |  | ||||||
|     if (!descendantSet.has(childTx)) { |  | ||||||
|       descendants.push(childTx); |  | ||||||
|       descendantSet.add(childTx); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   while (descendants.length) { |  | ||||||
|     descendantTx = descendants.pop(); |  | ||||||
|     if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) { |  | ||||||
|       // remove tx as ancestor
 |  | ||||||
|       descendantTx.ancestorMap.delete(rootTx.txid); |  | ||||||
|       descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta); |  | ||||||
|       descendantTx.ancestorVsize -= rootTx.adjustedVsize; |  | ||||||
|       descendantTx.ancestorSigops -= rootTx.sigops; |  | ||||||
|       descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize; |  | ||||||
|       descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate; |  | ||||||
| 
 |  | ||||||
|       if (!descendantTx.modified) { |  | ||||||
|         descendantTx.modified = true; |  | ||||||
|         modified.push(descendantTx); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // add this node's children to the stack
 |  | ||||||
|       descendantTx.children.forEach(childTx => { |  | ||||||
|         // visit each node only once
 |  | ||||||
|         if (!descendantSet.has(childTx)) { |  | ||||||
|           descendants.push(childTx); |  | ||||||
|           descendantSet.add(childTx); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   // return new, resorted modified list
 |  | ||||||
|   return modified.sort(priorityComparator); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Used to sort an array of MinerTransactions by descending ancestor score
 |  | ||||||
| function priorityComparator(a: MinerTransaction, b: MinerTransaction): number { |  | ||||||
|   if (b.score === a.score) { |  | ||||||
|     // tie-break by txid for stability
 |  | ||||||
|     return a.order - b.order; |  | ||||||
|   } else { |  | ||||||
|     return b.score - a.score; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // returns the most significant 4 bytes of the txid as an integer
 |  | ||||||
| function txidToOrdering(txid: string): number { |  | ||||||
|   return parseInt( |  | ||||||
|     txid.substring(62, 64) + |  | ||||||
|       txid.substring(60, 62) + |  | ||||||
|       txid.substring(58, 60) + |  | ||||||
|       txid.substring(56, 58), |  | ||||||
|     16 |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -19,7 +19,7 @@ import bitcoinClient from './bitcoin-client'; | |||||||
| import difficultyAdjustment from '../difficulty-adjustment'; | import difficultyAdjustment from '../difficulty-adjustment'; | ||||||
| import transactionRepository from '../../repositories/TransactionRepository'; | import transactionRepository from '../../repositories/TransactionRepository'; | ||||||
| import rbfCache from '../rbf-cache'; | import rbfCache from '../rbf-cache'; | ||||||
| import { calculateCpfp } from '../cpfp'; | import { calculateMempoolTxCpfp } from '../cpfp'; | ||||||
| 
 | 
 | ||||||
| class BitcoinRoutes { | class BitcoinRoutes { | ||||||
|   public initRoutes(app: Application) { |   public initRoutes(app: Application) { | ||||||
| @ -168,7 +168,7 @@ class BitcoinRoutes { | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const cpfpInfo = calculateCpfp(tx, mempool.getMempool()); |       const cpfpInfo = calculateMempoolTxCpfp(tx, mempool.getMempool()); | ||||||
| 
 | 
 | ||||||
|       res.json(cpfpInfo); |       res.json(cpfpInfo); | ||||||
|       return; |       return; | ||||||
|  | |||||||
| @ -30,6 +30,9 @@ import redisCache from './redis-cache'; | |||||||
| import rbfCache from './rbf-cache'; | import rbfCache from './rbf-cache'; | ||||||
| import { calcBitsDifference } from './difficulty-adjustment'; | import { calcBitsDifference } from './difficulty-adjustment'; | ||||||
| import AccelerationRepository from '../repositories/AccelerationRepository'; | import AccelerationRepository from '../repositories/AccelerationRepository'; | ||||||
|  | import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; | ||||||
|  | import mempool from './mempool'; | ||||||
|  | import CpfpRepository from '../repositories/CpfpRepository'; | ||||||
| 
 | 
 | ||||||
| class Blocks { | class Blocks { | ||||||
|   private blocks: BlockExtended[] = []; |   private blocks: BlockExtended[] = []; | ||||||
| @ -567,8 +570,11 @@ class Blocks { | |||||||
|     const blockchainInfo = await bitcoinClient.getBlockchainInfo(); |     const blockchainInfo = await bitcoinClient.getBlockchainInfo(); | ||||||
|     const currentBlockHeight = blockchainInfo.blocks; |     const currentBlockHeight = blockchainInfo.blocks; | ||||||
| 
 | 
 | ||||||
|     const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesWithVersion(0); |     const targetSummaryVersion: number = 1; | ||||||
|     const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesWithVersion(0); |     const targetTemplateVersion: number = 1; | ||||||
|  | 
 | ||||||
|  |     const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesBelowVersion(targetSummaryVersion); | ||||||
|  |     const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesBelowVersion(targetTemplateVersion); | ||||||
| 
 | 
 | ||||||
|     // nothing to do
 |     // nothing to do
 | ||||||
|     if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) { |     if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) { | ||||||
| @ -601,16 +607,24 @@ class Blocks { | |||||||
| 
 | 
 | ||||||
|     for (let height = currentBlockHeight; height >= 0; height--) { |     for (let height = currentBlockHeight; height >= 0; height--) { | ||||||
|       try { |       try { | ||||||
|         let txs: TransactionExtended[] | null = null; |         let txs: MempoolTransactionExtended[] | null = null; | ||||||
|         if (unclassifiedBlocks[height]) { |         if (unclassifiedBlocks[height]) { | ||||||
|           const blockHash = unclassifiedBlocks[height]; |           const blockHash = unclassifiedBlocks[height]; | ||||||
|           // fetch transactions
 |           // fetch transactions
 | ||||||
|           txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendTransaction(tx)) || []; |           txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendMempoolTransaction(tx)) || []; | ||||||
|           // add CPFP
 |           // add CPFP
 | ||||||
|           const cpfpSummary = Common.calculateCpfp(height, txs, true); |           const cpfpSummary = calculateGoodBlockCpfp(height, txs, []); | ||||||
|           // classify
 |           // classify
 | ||||||
|           const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); |           const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); | ||||||
|           await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 1); |           await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2); | ||||||
|  |           if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) { | ||||||
|  |             const cpfpClusters = await CpfpRepository.$getClustersAt(height); | ||||||
|  |             if (!cpfpRepository.compareClusters(cpfpClusters, cpfpSummary.clusters)) { | ||||||
|  |               // CPFP clusters changed - update the compact_cpfp tables
 | ||||||
|  |               await CpfpRepository.$deleteClustersAt(height); | ||||||
|  |               await this.$saveCpfp(blockHash, height, cpfpSummary); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|           await Common.sleep$(250); |           await Common.sleep$(250); | ||||||
|         } |         } | ||||||
|         if (unclassifiedTemplates[height]) { |         if (unclassifiedTemplates[height]) { | ||||||
| @ -636,7 +650,7 @@ class Blocks { | |||||||
|               } |               } | ||||||
|               templateTxs.push(tx || templateTx); |               templateTxs.push(tx || templateTx); | ||||||
|             } |             } | ||||||
|             const cpfpSummary = Common.calculateCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as TransactionExtended[], true); |             const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []); | ||||||
|             // classify
 |             // classify
 | ||||||
|             const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); |             const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); | ||||||
|             const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; |             const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; | ||||||
| @ -890,7 +904,7 @@ class Blocks { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions); |       const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, Object.values(mempool.getAccelerations()).map(a => ({ txid: a.txid, max_bid: a.feeDelta }))); | ||||||
|       const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); |       const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); | ||||||
|       const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); |       const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); | ||||||
|       this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); |       this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); | ||||||
| @ -1149,7 +1163,7 @@ class Blocks { | |||||||
|         transactions: cpfpSummary.transactions.map(tx => { |         transactions: cpfpSummary.transactions.map(tx => { | ||||||
|           let flags: number = 0; |           let flags: number = 0; | ||||||
|           try { |           try { | ||||||
|             flags = tx.flags || Common.getTransactionFlags(tx); |             flags = Common.getTransactionFlags(tx); | ||||||
|           } catch (e) { |           } catch (e) { | ||||||
|             logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); |             logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); | ||||||
|           } |           } | ||||||
| @ -1399,7 +1413,7 @@ class Blocks { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (transactions?.length != null) { |     if (transactions?.length != null) { | ||||||
|       const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]); |       const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]); | ||||||
| 
 | 
 | ||||||
|       await this.$saveCpfp(hash, height, summary); |       await this.$saveCpfp(hash, height, summary); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -419,12 +419,15 @@ export class Common { | |||||||
|     let flags = tx.flags ? BigInt(tx.flags) : 0n; |     let flags = tx.flags ? BigInt(tx.flags) : 0n; | ||||||
| 
 | 
 | ||||||
|     // Update variable flags (CPFP, RBF)
 |     // Update variable flags (CPFP, RBF)
 | ||||||
|  |     flags &= ~TransactionFlags.cpfp_child; | ||||||
|     if (tx.ancestors?.length) { |     if (tx.ancestors?.length) { | ||||||
|       flags |= TransactionFlags.cpfp_child; |       flags |= TransactionFlags.cpfp_child; | ||||||
|     } |     } | ||||||
|  |     flags &= ~TransactionFlags.cpfp_parent; | ||||||
|     if (tx.descendants?.length) { |     if (tx.descendants?.length) { | ||||||
|       flags |= TransactionFlags.cpfp_parent; |       flags |= TransactionFlags.cpfp_parent; | ||||||
|     } |     } | ||||||
|  |     flags &= ~TransactionFlags.replacement; | ||||||
|     if (tx.replacement) { |     if (tx.replacement) { | ||||||
|       flags |= TransactionFlags.replacement; |       flags |= TransactionFlags.replacement; | ||||||
|     } |     } | ||||||
| @ -806,96 +809,6 @@ export class Common { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static calculateCpfp(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, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats { |   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 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); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,29 +1,172 @@ | |||||||
| import { CpfpInfo, MempoolTransactionExtended } from '../mempool.interfaces'; | import { Ancestor, CpfpCluster, CpfpInfo, CpfpSummary, MempoolTransactionExtended, TransactionExtended } from '../mempool.interfaces'; | ||||||
|  | import { GraphTx, convertToGraphTx, expandRelativesGraph, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from './mini-miner'; | ||||||
| import memPool from './mempool'; | import memPool from './mempool'; | ||||||
|  | import { Acceleration } from './acceleration/acceleration'; | ||||||
| 
 | 
 | ||||||
| const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
 | const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
 | ||||||
| const MAX_GRAPH_SIZE = 50; // the maximum number of in-mempool relatives to consider
 | const MAX_CLUSTER_ITERATIONS = 100; | ||||||
| 
 | 
 | ||||||
| interface GraphTx extends MempoolTransactionExtended { | export function calculateFastBlockCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary { | ||||||
|   depends: string[]; |   const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
 | ||||||
|   spentby: string[]; |   const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
 | ||||||
|   ancestorMap: Map<string, GraphTx>; |   let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
 | ||||||
|   fees: { |   let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
 | ||||||
|     base: number; |   const txMap: { [txid: string]: TransactionExtended } = {}; | ||||||
|     ancestor: number; |   // 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: 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, | ||||||
|   }; |   }; | ||||||
|   ancestorcount: number; |  | ||||||
|   ancestorsize: number; |  | ||||||
|   ancestorRate: number; |  | ||||||
|   individualRate: number; |  | ||||||
|   score: number; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for |  * 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) |  * that transaction (and all others in the same cluster) | ||||||
|  */ |  */ | ||||||
| export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo { | export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo { | ||||||
|   if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) { |   if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) { | ||||||
|     tx.cpfpDirty = false; |     tx.cpfpDirty = false; | ||||||
|     return { |     return { | ||||||
| @ -38,24 +181,24 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const ancestorMap = new Map<string, GraphTx>(); |   const ancestorMap = new Map<string, GraphTx>(); | ||||||
|   const graphTx = mempoolToGraphTx(tx); |   const graphTx = convertToGraphTx(tx, memPool.getSpendMap()); | ||||||
|   ancestorMap.set(tx.txid, graphTx); |   ancestorMap.set(tx.txid, graphTx); | ||||||
| 
 | 
 | ||||||
|   const allRelatives = expandRelativesGraph(mempool, ancestorMap); |   const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap()); | ||||||
|   const relativesMap = initializeRelatives(allRelatives); |   const relativesMap = initializeRelatives(allRelatives); | ||||||
|   const cluster = calculateCpfpCluster(tx.txid, relativesMap); |   const cluster = calculateCpfpCluster(tx.txid, relativesMap); | ||||||
| 
 | 
 | ||||||
|   let totalVsize = 0; |   let totalVsize = 0; | ||||||
|   let totalFee = 0; |   let totalFee = 0; | ||||||
|   for (const tx of cluster.values()) { |   for (const tx of cluster.values()) { | ||||||
|     totalVsize += tx.adjustedVsize; |     totalVsize += tx.vsize; | ||||||
|     totalFee += tx.fee; |     totalFee += tx.fees.base; | ||||||
|   } |   } | ||||||
|   const effectiveFeePerVsize = totalFee / totalVsize; |   const effectiveFeePerVsize = totalFee / totalVsize; | ||||||
|   for (const tx of cluster.values()) { |   for (const tx of cluster.values()) { | ||||||
|     mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; |     mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; | ||||||
|     mempool[tx.txid].ancestors = Array.from(tx.ancestorMap.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee })); |     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.ancestorMap.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee })); |     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].bestDescendant = null; | ||||||
|     mempool[tx.txid].cpfpChecked = true; |     mempool[tx.txid].cpfpChecked = true; | ||||||
|     mempool[tx.txid].cpfpDirty = true; |     mempool[tx.txid].cpfpDirty = true; | ||||||
| @ -75,83 +218,6 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: | |||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function mempoolToGraphTx(tx: MempoolTransactionExtended): GraphTx { |  | ||||||
|   return { |  | ||||||
|     ...tx, |  | ||||||
|     depends: tx.vin.map(v => v.txid), |  | ||||||
|     spentby: tx.vout.map((v, i) => memPool.getFromSpendMap(tx.txid, i)).map(tx => tx?.txid).filter(txid => txid != null) as string[], |  | ||||||
|     ancestorMap: new Map(), |  | ||||||
|     fees: { |  | ||||||
|       base: tx.fee, |  | ||||||
|       ancestor: tx.fee, |  | ||||||
|     }, |  | ||||||
|     ancestorcount: 1, |  | ||||||
|     ancestorsize: tx.adjustedVsize, |  | ||||||
|     ancestorRate: 0, |  | ||||||
|     individualRate: 0, |  | ||||||
|     score: 0, |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives |  | ||||||
|  */ |  | ||||||
| function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>): Map<string, GraphTx> { |  | ||||||
|   const relatives: Map<string, GraphTx> = new Map(); |  | ||||||
|   const stack: GraphTx[] = Array.from(ancestors.values()); |  | ||||||
|   while (stack.length > 0) { |  | ||||||
|     if (relatives.size > MAX_GRAPH_SIZE) { |  | ||||||
|       return relatives; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const nextTx = stack.pop(); |  | ||||||
|     if (!nextTx) { |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|     relatives.set(nextTx.txid, nextTx); |  | ||||||
| 
 |  | ||||||
|     for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) { |  | ||||||
|       if (relatives.has(relativeTxid)) { |  | ||||||
|         // already processed this tx
 |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|       let mempoolTx = ancestors.get(relativeTxid); |  | ||||||
|       if (!mempoolTx && mempool[relativeTxid]) { |  | ||||||
|         mempoolTx = mempoolToGraphTx(mempool[relativeTxid]); |  | ||||||
|       } |  | ||||||
|       if (mempoolTx) { |  | ||||||
|         stack.push(mempoolTx); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return relatives; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
|  /** |  | ||||||
|    * Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph |  | ||||||
|    * by running setAncestors on each leaf, and caching intermediate results. |  | ||||||
|    * then initializes ancestor data for each transaction |  | ||||||
|    *  |  | ||||||
|    * @param all  |  | ||||||
|    */ |  | ||||||
|  function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> { |  | ||||||
|   const visited: Map<string, Map<string, GraphTx>> = new Map(); |  | ||||||
|   const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0); |  | ||||||
|   for (const leaf of leaves) { |  | ||||||
|     setAncestors(leaf, mempoolTxs, visited); |  | ||||||
|   } |  | ||||||
|   mempoolTxs.forEach(entry => { |  | ||||||
|     entry.ancestorMap?.forEach(ancestor => { |  | ||||||
|       entry.ancestorcount++; |  | ||||||
|       entry.ancestorsize += ancestor.adjustedVsize; |  | ||||||
|       entry.fees.ancestor += ancestor.fees.base; |  | ||||||
|     }); |  | ||||||
|     setAncestorScores(entry); |  | ||||||
|   }); |  | ||||||
|   return mempoolTxs; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|    * Given a root transaction and a list of in-mempool ancestors, |    * Given a root transaction and a list of in-mempool ancestors, | ||||||
|    * Calculate the CPFP cluster |    * Calculate the CPFP cluster | ||||||
| @ -172,10 +238,10 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st | |||||||
|   let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator); |   let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator); | ||||||
| 
 | 
 | ||||||
|   // Iterate until we reach a cluster that includes our target tx
 |   // Iterate until we reach a cluster that includes our target tx
 | ||||||
|   let maxIterations = MAX_GRAPH_SIZE; |   let maxIterations = MAX_CLUSTER_ITERATIONS; | ||||||
|   let best = sortedRelatives.shift(); |   let best = sortedRelatives.shift(); | ||||||
|   let bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []); |   let bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []); | ||||||
|   while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestorMap.has(tx.txid)) && maxIterations > 0) { |   while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestors.has(tx.txid)) && maxIterations > 0) { | ||||||
|     maxIterations--; |     maxIterations--; | ||||||
|     if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) { |     if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) { | ||||||
|       break; |       break; | ||||||
| @ -190,7 +256,7 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st | |||||||
|       // Grab the next highest scoring entry
 |       // Grab the next highest scoring entry
 | ||||||
|       best = sortedRelatives.shift(); |       best = sortedRelatives.shift(); | ||||||
|       if (best) { |       if (best) { | ||||||
|         bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []); |         bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []); | ||||||
|         bestCluster.set(best?.txid, best); |         bestCluster.set(best?.txid, best); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -199,88 +265,4 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st | |||||||
|   bestCluster.set(tx.txid, tx); |   bestCluster.set(tx.txid, tx); | ||||||
| 
 | 
 | ||||||
|   return bestCluster; |   return bestCluster; | ||||||
| } |  | ||||||
| 
 |  | ||||||
|  /** |  | ||||||
|    * Remove a cluster of transactions from an in-mempool dependency graph |  | ||||||
|    * and update the survivors' scores and ancestors |  | ||||||
|    *  |  | ||||||
|    * @param cluster  |  | ||||||
|    * @param ancestors  |  | ||||||
|    */ |  | ||||||
|  function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void { |  | ||||||
|   // remove
 |  | ||||||
|   cluster.forEach(tx => { |  | ||||||
|     all.delete(tx.txid); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   // update survivors
 |  | ||||||
|   all.forEach(tx => { |  | ||||||
|     cluster.forEach(remove => { |  | ||||||
|       if (tx.ancestorMap?.has(remove.txid)) { |  | ||||||
|         // remove as dependency
 |  | ||||||
|         tx.ancestorMap.delete(remove.txid); |  | ||||||
|         tx.depends = tx.depends.filter(parent => parent !== remove.txid); |  | ||||||
|         // update ancestor sizes and fees
 |  | ||||||
|         tx.ancestorsize -= remove.adjustedVsize; |  | ||||||
|         tx.fees.ancestor -= remove.fees.base; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     // recalculate fee rates
 |  | ||||||
|     setAncestorScores(tx); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|    * Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors |  | ||||||
|    * for each transaction. |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    * @param all  |  | ||||||
|    */ |  | ||||||
| function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> { |  | ||||||
|   // sanity check for infinite recursion / too many ancestors (should never happen)
 |  | ||||||
|   if (depth > MAX_GRAPH_SIZE) { |  | ||||||
|     return tx.ancestorMap; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   // initialize the ancestor map for this tx
 |  | ||||||
|   tx.ancestorMap = new Map<string, GraphTx>(); |  | ||||||
|   tx.depends.forEach(parentId => { |  | ||||||
|     const parent = all.get(parentId); |  | ||||||
|     if (parent) { |  | ||||||
|       // add the parent
 |  | ||||||
|       tx.ancestorMap?.set(parentId, parent); |  | ||||||
|       // check for a cached copy of this parent's ancestors
 |  | ||||||
|       let ancestors = visited.get(parent.txid); |  | ||||||
|       if (!ancestors) { |  | ||||||
|         // recursively fetch the parent's ancestors
 |  | ||||||
|         ancestors = setAncestors(parent, all, visited, depth + 1); |  | ||||||
|       } |  | ||||||
|       // and add to this tx's map
 |  | ||||||
|       ancestors.forEach((ancestor, ancestorId) => { |  | ||||||
|         tx.ancestorMap?.set(ancestorId, ancestor); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   visited.set(tx.txid, tx.ancestorMap); |  | ||||||
| 
 |  | ||||||
|   return tx.ancestorMap; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|    * Take a mempool transaction, and set the fee rates and ancestor score |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    */ |  | ||||||
| function setAncestorScores(tx: GraphTx): GraphTx { |  | ||||||
|   tx.individualRate = (tx.fees.base * 100_000_000) / tx.adjustedVsize; |  | ||||||
|   tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / tx.ancestorsize; |  | ||||||
|   tx.score = Math.min(tx.individualRate, tx.ancestorRate); |  | ||||||
|   return tx; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Sort by descending score
 |  | ||||||
| function mempoolComparator(a: GraphTx, b: GraphTx): number { |  | ||||||
|   return b.score - a.score; |  | ||||||
| } | } | ||||||
							
								
								
									
										515
									
								
								backend/src/api/mini-miner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										515
									
								
								backend/src/api/mini-miner.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,515 @@ | |||||||
|  | import { Acceleration } from './acceleration/acceleration'; | ||||||
|  | import { MempoolTransactionExtended } from '../mempool.interfaces'; | ||||||
|  | import logger from '../logger'; | ||||||
|  | 
 | ||||||
|  | const BLOCK_WEIGHT_UNITS = 4_000_000; | ||||||
|  | const BLOCK_SIGOPS = 80_000; | ||||||
|  | const MAX_RELATIVE_GRAPH_SIZE = 100; | ||||||
|  | 
 | ||||||
|  | export interface GraphTx { | ||||||
|  |   txid: string; | ||||||
|  |   vsize: number; | ||||||
|  |   weight: number; | ||||||
|  |   depends: string[]; | ||||||
|  |   spentby: string[]; | ||||||
|  | 
 | ||||||
|  |   ancestorcount: number; | ||||||
|  |   ancestorsize: number; | ||||||
|  |   fees: { // in sats
 | ||||||
|  |     base: number; | ||||||
|  |     ancestor: number; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   ancestors: Map<string, GraphTx>, | ||||||
|  |   ancestorRate: number; | ||||||
|  |   individualRate: number; | ||||||
|  |   score: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface TemplateTransaction { | ||||||
|  |   txid: string; | ||||||
|  |   order: number; | ||||||
|  |   weight: number; | ||||||
|  |   adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
 | ||||||
|  |   sigops: number; | ||||||
|  |   fee: number; | ||||||
|  |   feeDelta: number; | ||||||
|  |   ancestors: string[]; | ||||||
|  |   cluster: string[]; | ||||||
|  |   effectiveFeePerVsize: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface MinerTransaction extends TemplateTransaction { | ||||||
|  |   inputs: string[]; | ||||||
|  |   feePerVsize: number; | ||||||
|  |   relativesSet: boolean; | ||||||
|  |   ancestorMap: Map<string, MinerTransaction>; | ||||||
|  |   children: Set<MinerTransaction>; | ||||||
|  |   ancestorFee: number; | ||||||
|  |   ancestorVsize: number; | ||||||
|  |   ancestorSigops: number; | ||||||
|  |   score: number; | ||||||
|  |   used: boolean; | ||||||
|  |   modified: boolean; | ||||||
|  |   dependencyRate: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Takes a raw transaction, and builds a graph of same-block relatives, | ||||||
|  |  * and returns as a GraphTx | ||||||
|  |  * | ||||||
|  |  * @param tx | ||||||
|  |  */ | ||||||
|  | export function getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> { | ||||||
|  |   const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
 | ||||||
|  |   const spendMap = new Map<string, string>(); // map of outpoints to spending txids
 | ||||||
|  |   for (const tx of transactions) { | ||||||
|  |     blockTxs.set(tx.txid, tx); | ||||||
|  |     for (const vin of tx.vin) { | ||||||
|  |       spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const relatives: Map<string, GraphTx> = new Map(); | ||||||
|  |   const stack: string[] = [tx.txid]; | ||||||
|  | 
 | ||||||
|  |   // build set of same-block ancestors
 | ||||||
|  |   while (stack.length > 0) { | ||||||
|  |     const nextTxid = stack.pop(); | ||||||
|  |     const nextTx = nextTxid ? blockTxs.get(nextTxid) : null; | ||||||
|  |     if (!nextTx || relatives.has(nextTx.txid)) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const mempoolTx = convertToGraphTx(nextTx, spendMap); | ||||||
|  | 
 | ||||||
|  |     for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) { | ||||||
|  |       if (txid) { | ||||||
|  |         stack.push(txid); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     relatives.set(mempoolTx.txid, mempoolTx); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return relatives; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Takes a raw transaction and converts it to GraphTx format | ||||||
|  |  * fee and ancestor data is initialized with dummy/null values | ||||||
|  |  * | ||||||
|  |  * @param tx | ||||||
|  |  */ | ||||||
|  | export function convertToGraphTx(tx: MempoolTransactionExtended, spendMap?: Map<string, MempoolTransactionExtended | string>): GraphTx { | ||||||
|  |   return { | ||||||
|  |     txid: tx.txid, | ||||||
|  |     vsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)), | ||||||
|  |     weight: tx.weight, | ||||||
|  |     fees: { | ||||||
|  |       base: tx.fee || 0, | ||||||
|  |       ancestor: tx.fee || 0, | ||||||
|  |     }, | ||||||
|  |     depends: (tx.vin.map(vin => vin.txid).filter(depend => depend) as string[]), | ||||||
|  |     spentby: spendMap ? (tx.vout.map((vout, index) => { const spend = spendMap.get(`${tx.txid}:${index}`); return (spend?.['txid'] || spend); }).filter(spent => spent) as string[]) : [], | ||||||
|  | 
 | ||||||
|  |     ancestorcount: 1, | ||||||
|  |     ancestorsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)), | ||||||
|  |     ancestors: new Map<string, GraphTx>(), | ||||||
|  |     ancestorRate: 0, | ||||||
|  |     individualRate: 0, | ||||||
|  |     score: 0, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives | ||||||
|  |  */ | ||||||
|  | export function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>, spendMap: Map<string, MempoolTransactionExtended>): Map<string, GraphTx> { | ||||||
|  |   const relatives: Map<string, GraphTx> = new Map(); | ||||||
|  |   const stack: GraphTx[] = Array.from(ancestors.values()); | ||||||
|  |   while (stack.length > 0) { | ||||||
|  |     if (relatives.size > MAX_RELATIVE_GRAPH_SIZE) { | ||||||
|  |       return relatives; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const nextTx = stack.pop(); | ||||||
|  |     if (!nextTx) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |     relatives.set(nextTx.txid, nextTx); | ||||||
|  | 
 | ||||||
|  |     for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) { | ||||||
|  |       if (relatives.has(relativeTxid)) { | ||||||
|  |         // already processed this tx
 | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |       let ancestorTx = ancestors.get(relativeTxid); | ||||||
|  |       if (!ancestorTx && relativeTxid in mempool) { | ||||||
|  |         const mempoolTx = mempool[relativeTxid]; | ||||||
|  |         ancestorTx = convertToGraphTx(mempoolTx, spendMap); | ||||||
|  |       } | ||||||
|  |       if (ancestorTx) { | ||||||
|  |         stack.push(ancestorTx); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return relatives; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors | ||||||
|  |  * for each transaction. | ||||||
|  |  * | ||||||
|  |  * @param tx | ||||||
|  |  * @param all | ||||||
|  |  */ | ||||||
|  | function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> { | ||||||
|  |   // sanity check for infinite recursion / too many ancestors (should never happen)
 | ||||||
|  |   if (depth > MAX_RELATIVE_GRAPH_SIZE) { | ||||||
|  |     logger.warn('cpfp dependency calculation failed: setAncestors reached depth of 100, unable to proceed'); | ||||||
|  |     return tx.ancestors; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // initialize the ancestor map for this tx
 | ||||||
|  |   tx.ancestors = new Map<string, GraphTx>(); | ||||||
|  |   tx.depends.forEach(parentId => { | ||||||
|  |     const parent = all.get(parentId); | ||||||
|  |     if (parent) { | ||||||
|  |       // add the parent
 | ||||||
|  |       tx.ancestors?.set(parentId, parent); | ||||||
|  |       // check for a cached copy of this parent's ancestors
 | ||||||
|  |       let ancestors = visited.get(parent.txid); | ||||||
|  |       if (!ancestors) { | ||||||
|  |         // recursively fetch the parent's ancestors
 | ||||||
|  |         ancestors = setAncestors(parent, all, visited, depth + 1); | ||||||
|  |       } | ||||||
|  |       // and add to this tx's map
 | ||||||
|  |       ancestors.forEach((ancestor, ancestorId) => { | ||||||
|  |         tx.ancestors?.set(ancestorId, ancestor); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   visited.set(tx.txid, tx.ancestors); | ||||||
|  | 
 | ||||||
|  |   return tx.ancestors; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |    * Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph | ||||||
|  |    * by running setAncestors on each leaf, and caching intermediate results. | ||||||
|  |    * then initializes ancestor data for each transaction | ||||||
|  |    * | ||||||
|  |    * @param all | ||||||
|  |    */ | ||||||
|  | export function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> { | ||||||
|  |   const visited: Map<string, Map<string, GraphTx>> = new Map(); | ||||||
|  |   const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0); | ||||||
|  |   for (const leaf of leaves) { | ||||||
|  |     setAncestors(leaf, mempoolTxs, visited); | ||||||
|  |   } | ||||||
|  |   mempoolTxs.forEach(entry => { | ||||||
|  |     entry.ancestors?.forEach(ancestor => { | ||||||
|  |       entry.ancestorcount++; | ||||||
|  |       entry.ancestorsize += ancestor.vsize; | ||||||
|  |       entry.fees.ancestor += ancestor.fees.base; | ||||||
|  |     }); | ||||||
|  |     setAncestorScores(entry); | ||||||
|  |   }); | ||||||
|  |   return mempoolTxs; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Remove a cluster of transactions from an in-mempool dependency graph | ||||||
|  |  * and update the survivors' scores and ancestors | ||||||
|  |  * | ||||||
|  |  * @param cluster | ||||||
|  |  * @param ancestors | ||||||
|  |  */ | ||||||
|  | export function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void { | ||||||
|  |   // remove
 | ||||||
|  |   cluster.forEach(tx => { | ||||||
|  |     all.delete(tx.txid); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // update survivors
 | ||||||
|  |   all.forEach(tx => { | ||||||
|  |     cluster.forEach(remove => { | ||||||
|  |       if (tx.ancestors?.has(remove.txid)) { | ||||||
|  |         // remove as dependency
 | ||||||
|  |         tx.ancestors.delete(remove.txid); | ||||||
|  |         tx.depends = tx.depends.filter(parent => parent !== remove.txid); | ||||||
|  |         // update ancestor sizes and fees
 | ||||||
|  |         tx.ancestorsize -= remove.vsize; | ||||||
|  |         tx.fees.ancestor -= remove.fees.base; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     // recalculate fee rates
 | ||||||
|  |     setAncestorScores(tx); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Take a mempool transaction, and set the fee rates and ancestor score | ||||||
|  |  * | ||||||
|  |  * @param tx | ||||||
|  |  */ | ||||||
|  | export function setAncestorScores(tx: GraphTx): void { | ||||||
|  |   tx.individualRate = tx.fees.base / tx.vsize; | ||||||
|  |   tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize; | ||||||
|  |   tx.score = Math.min(tx.individualRate, tx.ancestorRate); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Sort by descending score
 | ||||||
|  | export function mempoolComparator(a: GraphTx, b: GraphTx): number { | ||||||
|  |   return b.score - a.score; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | * Build a block using an approximation of the transaction selection algorithm from Bitcoin Core | ||||||
|  | * (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
 | ||||||
|  | */ | ||||||
|  | export function makeBlockTemplate(candidates: MempoolTransactionExtended[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] { | ||||||
|  |   const auditPool: Map<string, MinerTransaction> = new Map(); | ||||||
|  |   const mempoolArray: MinerTransaction[] = []; | ||||||
|  | 
 | ||||||
|  |   candidates.forEach(tx => { | ||||||
|  |     // initializing everything up front helps V8 optimize property access later
 | ||||||
|  |     const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0))); | ||||||
|  |     const feePerVsize = (tx.fee / adjustedVsize); | ||||||
|  |     auditPool.set(tx.txid, { | ||||||
|  |       txid: tx.txid, | ||||||
|  |       order: txidToOrdering(tx.txid), | ||||||
|  |       fee: tx.fee, | ||||||
|  |       feeDelta: 0, | ||||||
|  |       weight: tx.weight, | ||||||
|  |       adjustedVsize, | ||||||
|  |       feePerVsize: feePerVsize, | ||||||
|  |       effectiveFeePerVsize: feePerVsize, | ||||||
|  |       dependencyRate: feePerVsize, | ||||||
|  |       sigops: tx.sigops || 0, | ||||||
|  |       inputs: (tx.vin?.map(vin => vin.txid) || []) as string[], | ||||||
|  |       relativesSet: false, | ||||||
|  |       ancestors: [], | ||||||
|  |       cluster: [], | ||||||
|  |       ancestorMap: new Map<string, MinerTransaction>(), | ||||||
|  |       children: new Set<MinerTransaction>(), | ||||||
|  |       ancestorFee: 0, | ||||||
|  |       ancestorVsize: 0, | ||||||
|  |       ancestorSigops: 0, | ||||||
|  |       score: 0, | ||||||
|  |       used: false, | ||||||
|  |       modified: false, | ||||||
|  |     }); | ||||||
|  |     mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // set accelerated effective fee
 | ||||||
|  |   for (const acceleration of accelerations) { | ||||||
|  |     const tx = auditPool.get(acceleration.txid); | ||||||
|  |     if (tx) { | ||||||
|  |       tx.feeDelta = acceleration.max_bid; | ||||||
|  |       tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize); | ||||||
|  |       tx.effectiveFeePerVsize = tx.feePerVsize; | ||||||
|  |       tx.dependencyRate = tx.feePerVsize; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Build relatives graph & calculate ancestor scores
 | ||||||
|  |   for (const tx of mempoolArray) { | ||||||
|  |     if (!tx.relativesSet) { | ||||||
|  |       setRelatives(tx, auditPool); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Sort by descending ancestor score
 | ||||||
|  |   mempoolArray.sort(priorityComparator); | ||||||
|  | 
 | ||||||
|  |   // Build blocks by greedily choosing the highest feerate package
 | ||||||
|  |   // (i.e. the package rooted in the transaction with the best ancestor score)
 | ||||||
|  |   const blocks: number[][] = []; | ||||||
|  |   let blockWeight = 0; | ||||||
|  |   let blockSigops = 0; | ||||||
|  |   const transactions: MinerTransaction[] = []; | ||||||
|  |   let modified: MinerTransaction[] = []; | ||||||
|  |   const overflow: MinerTransaction[] = []; | ||||||
|  |   let failures = 0; | ||||||
|  |   while (mempoolArray.length || modified.length) { | ||||||
|  |     // skip invalid transactions
 | ||||||
|  |     while (mempoolArray[0].used || mempoolArray[0].modified) { | ||||||
|  |       mempoolArray.shift(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Select best next package
 | ||||||
|  |     let nextTx; | ||||||
|  |     const nextPoolTx = mempoolArray[0]; | ||||||
|  |     const nextModifiedTx = modified[0]; | ||||||
|  |     if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) { | ||||||
|  |       nextTx = nextPoolTx; | ||||||
|  |       mempoolArray.shift(); | ||||||
|  |     } else { | ||||||
|  |       modified.shift(); | ||||||
|  |       if (nextModifiedTx) { | ||||||
|  |         nextTx = nextModifiedTx; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (nextTx && !nextTx?.used) { | ||||||
|  |       // Check if the package fits into this block
 | ||||||
|  |       if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) { | ||||||
|  |         const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values()); | ||||||
|  |         // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
 | ||||||
|  |         const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx]; | ||||||
|  |         const clusterTxids = sortedTxSet.map(tx => tx.txid); | ||||||
|  |         const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize); | ||||||
|  |         const used: MinerTransaction[] = []; | ||||||
|  |         while (sortedTxSet.length) { | ||||||
|  |           const ancestor = sortedTxSet.pop(); | ||||||
|  |           if (!ancestor) { | ||||||
|  |             continue; | ||||||
|  |           } | ||||||
|  |           ancestor.used = true; | ||||||
|  |           ancestor.usedBy = nextTx.txid; | ||||||
|  |           // update this tx with effective fee rate & relatives data
 | ||||||
|  |           if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) { | ||||||
|  |             ancestor.effectiveFeePerVsize = effectiveFeeRate; | ||||||
|  |           } | ||||||
|  |           ancestor.cluster = clusterTxids; | ||||||
|  |           transactions.push(ancestor); | ||||||
|  |           blockWeight += ancestor.weight; | ||||||
|  |           blockSigops += ancestor.sigops; | ||||||
|  |           used.push(ancestor); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // remove these as valid package ancestors for any descendants remaining in the mempool
 | ||||||
|  |         if (used.length) { | ||||||
|  |           used.forEach(tx => { | ||||||
|  |             modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         failures = 0; | ||||||
|  |       } else { | ||||||
|  |         // hold this package in an overflow list while we check for smaller options
 | ||||||
|  |         overflow.push(nextTx); | ||||||
|  |         failures++; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // this block is full
 | ||||||
|  |     const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000); | ||||||
|  |     const queueEmpty = !mempoolArray.length && !modified.length; | ||||||
|  | 
 | ||||||
|  |     if (exceededPackageTries || queueEmpty) { | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   for (const tx of transactions) { | ||||||
|  |     tx.ancestors = Object.values(tx.ancestorMap); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return transactions; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // traverse in-mempool ancestors
 | ||||||
|  | // recursion unavoidable, but should be limited to depth < 25 by mempool policy
 | ||||||
|  | function setRelatives( | ||||||
|  |   tx: MinerTransaction, | ||||||
|  |   mempool: Map<string, MinerTransaction>, | ||||||
|  | ): void { | ||||||
|  |   for (const parent of tx.inputs) { | ||||||
|  |     const parentTx = mempool.get(parent); | ||||||
|  |     if (parentTx && !tx.ancestorMap?.has(parent)) { | ||||||
|  |       tx.ancestorMap.set(parent, parentTx); | ||||||
|  |       parentTx.children.add(tx); | ||||||
|  |       // visit each node only once
 | ||||||
|  |       if (!parentTx.relativesSet) { | ||||||
|  |         setRelatives(parentTx, mempool); | ||||||
|  |       } | ||||||
|  |       parentTx.ancestorMap.forEach((ancestor) => { | ||||||
|  |         tx.ancestorMap.set(ancestor.txid, ancestor); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |   tx.ancestorFee = (tx.fee + tx.feeDelta); | ||||||
|  |   tx.ancestorVsize = tx.adjustedVsize || 0; | ||||||
|  |   tx.ancestorSigops = tx.sigops || 0; | ||||||
|  |   tx.ancestorMap.forEach((ancestor) => { | ||||||
|  |     tx.ancestorFee += (ancestor.fee + ancestor.feeDelta); | ||||||
|  |     tx.ancestorVsize += ancestor.adjustedVsize; | ||||||
|  |     tx.ancestorSigops += ancestor.sigops; | ||||||
|  |   }); | ||||||
|  |   tx.score = tx.ancestorFee / tx.ancestorVsize; | ||||||
|  |   tx.relativesSet = true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
 | ||||||
|  | // avoids recursion to limit call stack depth
 | ||||||
|  | function updateDescendants( | ||||||
|  |   rootTx: MinerTransaction, | ||||||
|  |   mempool: Map<string, MinerTransaction>, | ||||||
|  |   modified: MinerTransaction[], | ||||||
|  |   clusterRate: number, | ||||||
|  | ): MinerTransaction[] { | ||||||
|  |   const descendantSet: Set<MinerTransaction> = new Set(); | ||||||
|  |   // stack of nodes left to visit
 | ||||||
|  |   const descendants: MinerTransaction[] = []; | ||||||
|  |   let descendantTx: MinerTransaction | undefined; | ||||||
|  |   rootTx.children.forEach(childTx => { | ||||||
|  |     if (!descendantSet.has(childTx)) { | ||||||
|  |       descendants.push(childTx); | ||||||
|  |       descendantSet.add(childTx); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   while (descendants.length) { | ||||||
|  |     descendantTx = descendants.pop(); | ||||||
|  |     if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) { | ||||||
|  |       // remove tx as ancestor
 | ||||||
|  |       descendantTx.ancestorMap.delete(rootTx.txid); | ||||||
|  |       descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta); | ||||||
|  |       descendantTx.ancestorVsize -= rootTx.adjustedVsize; | ||||||
|  |       descendantTx.ancestorSigops -= rootTx.sigops; | ||||||
|  |       descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize; | ||||||
|  |       descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate; | ||||||
|  | 
 | ||||||
|  |       if (!descendantTx.modified) { | ||||||
|  |         descendantTx.modified = true; | ||||||
|  |         modified.push(descendantTx); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // add this node's children to the stack
 | ||||||
|  |       descendantTx.children.forEach(childTx => { | ||||||
|  |         // visit each node only once
 | ||||||
|  |         if (!descendantSet.has(childTx)) { | ||||||
|  |           descendants.push(childTx); | ||||||
|  |           descendantSet.add(childTx); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // return new, resorted modified list
 | ||||||
|  |   return modified.sort(priorityComparator); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Used to sort an array of MinerTransactions by descending ancestor score
 | ||||||
|  | function priorityComparator(a: MinerTransaction, b: MinerTransaction): number { | ||||||
|  |   if (b.score === a.score) { | ||||||
|  |     // tie-break by txid for stability
 | ||||||
|  |     return a.order - b.order; | ||||||
|  |   } else { | ||||||
|  |     return b.score - a.score; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // returns the most significant 4 bytes of the txid as an integer
 | ||||||
|  | function txidToOrdering(txid: string): number { | ||||||
|  |   return parseInt( | ||||||
|  |     txid.substring(62, 64) + | ||||||
|  |       txid.substring(60, 62) + | ||||||
|  |       txid.substring(58, 60) + | ||||||
|  |       txid.substring(56, 58), | ||||||
|  |     16 | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @ -103,7 +103,7 @@ class TransactionUtils { | |||||||
|     } |     } | ||||||
|     const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4); |     const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4); | ||||||
|     const transactionExtended: TransactionExtended = Object.assign({ |     const transactionExtended: TransactionExtended = Object.assign({ | ||||||
|       vsize: Math.round(transaction.weight / 4), |       vsize: transaction.weight / 4, | ||||||
|       feePerVsize: feePerVbytes, |       feePerVsize: feePerVbytes, | ||||||
|       effectiveFeePerVsize: feePerVbytes, |       effectiveFeePerVsize: feePerVbytes, | ||||||
|     }, transaction); |     }, transaction); | ||||||
| @ -123,7 +123,7 @@ class TransactionUtils { | |||||||
|     const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize; |     const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize; | ||||||
|     const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, { |     const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, { | ||||||
|       order: this.txidToOrdering(transaction.txid), |       order: this.txidToOrdering(transaction.txid), | ||||||
|       vsize: Math.round(transaction.weight / 4), |       vsize, | ||||||
|       adjustedVsize, |       adjustedVsize, | ||||||
|       sigops, |       sigops, | ||||||
|       feePerVsize: feePerVbytes, |       feePerVsize: feePerVbytes, | ||||||
|  | |||||||
| @ -33,7 +33,7 @@ interface AddressTransactions { | |||||||
|   removed: MempoolTransactionExtended[], |   removed: MempoolTransactionExtended[], | ||||||
| } | } | ||||||
| import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; | import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; | ||||||
| import { calculateCpfp } from './cpfp'; | import { calculateMempoolTxCpfp } from './cpfp'; | ||||||
| 
 | 
 | ||||||
| // valid 'want' subscriptions
 | // valid 'want' subscriptions
 | ||||||
| const wantable = [ | const wantable = [ | ||||||
| @ -827,7 +827,7 @@ class WebsocketHandler { | |||||||
|             accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid), |             accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid), | ||||||
|           }; |           }; | ||||||
|           if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) { |           if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) { | ||||||
|             calculateCpfp(mempoolTx, newMempool); |             calculateMempoolTxCpfp(mempoolTx, newMempool); | ||||||
|           } |           } | ||||||
|           if (mempoolTx.cpfpDirty) { |           if (mempoolTx.cpfpDirty) { | ||||||
|             positionData['cpfp'] = { |             positionData['cpfp'] = { | ||||||
| @ -866,7 +866,7 @@ class WebsocketHandler { | |||||||
|               acceleratedAt: mempoolTx.acceleratedAt || undefined, |               acceleratedAt: mempoolTx.acceleratedAt || undefined, | ||||||
|             }; |             }; | ||||||
|             if (!mempoolTx.cpfpChecked) { |             if (!mempoolTx.cpfpChecked) { | ||||||
|               calculateCpfp(mempoolTx, newMempool); |               calculateMempoolTxCpfp(mempoolTx, newMempool); | ||||||
|             } |             } | ||||||
|             if (mempoolTx.cpfpDirty) { |             if (mempoolTx.cpfpDirty) { | ||||||
|               txInfo.cpfp = { |               txInfo.cpfp = { | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration'; | import { AccelerationInfo } from '../api/acceleration/acceleration'; | ||||||
| import { RowDataPacket } from 'mysql2'; | import { RowDataPacket } from 'mysql2'; | ||||||
| import DB from '../database'; | import DB from '../database'; | ||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
| @ -11,6 +11,7 @@ import accelerationCosts from '../api/acceleration/acceleration'; | |||||||
| import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; | import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; | ||||||
| import transactionUtils from '../api/transaction-utils'; | import transactionUtils from '../api/transaction-utils'; | ||||||
| import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces'; | import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces'; | ||||||
|  | import { makeBlockTemplate } from '../api/mini-miner'; | ||||||
| 
 | 
 | ||||||
| export interface PublicAcceleration { | export interface PublicAcceleration { | ||||||
|   txid: string, |   txid: string, | ||||||
|  | |||||||
| @ -114,6 +114,43 @@ class BlocksSummariesRepository { | |||||||
|     return []; |     return []; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $getSummariesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> { | ||||||
|  |     try { | ||||||
|  |       const [rows]: any[] = await DB.query(` | ||||||
|  |         SELECT | ||||||
|  |           height, | ||||||
|  |           id, | ||||||
|  |           version | ||||||
|  |         FROM blocks_summaries | ||||||
|  |         WHERE version < ? | ||||||
|  |         ORDER BY height DESC;`, [version]);
 | ||||||
|  |       return rows; | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async $getTemplatesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> { | ||||||
|  |     try { | ||||||
|  |       const [rows]: any[] = await DB.query(` | ||||||
|  |         SELECT | ||||||
|  |           blocks_summaries.height as height, | ||||||
|  |           blocks_templates.id as id, | ||||||
|  |           blocks_templates.version as version | ||||||
|  |         FROM blocks_templates | ||||||
|  |         JOIN blocks_summaries ON blocks_templates.id = blocks_summaries.id | ||||||
|  |         WHERE blocks_templates.version < ? | ||||||
|  |         ORDER BY height DESC;`, [version]);
 | ||||||
|  |       return rows; | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Get the fee percentiles if the block has already been indexed, [] otherwise |    * Get the fee percentiles if the block has already been indexed, [] otherwise | ||||||
|    *  |    *  | ||||||
|  | |||||||
| @ -91,6 +91,26 @@ class CpfpRepository { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $getClustersAt(height: number): Promise<CpfpCluster[]> { | ||||||
|  |     const [clusterRows]: any = await DB.query( | ||||||
|  |       ` | ||||||
|  |         SELECT * | ||||||
|  |         FROM compact_cpfp_clusters | ||||||
|  |         WHERE height = ? | ||||||
|  |       `,
 | ||||||
|  |       [height] | ||||||
|  |     ); | ||||||
|  |     return clusterRows.map(cluster => { | ||||||
|  |       if (cluster?.txs) { | ||||||
|  |         cluster.effectiveFeePerVsize = cluster.fee_rate; | ||||||
|  |         cluster.txs = this.unpack(cluster.txs); | ||||||
|  |         return cluster; | ||||||
|  |       } else { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |     }).filter(cluster => cluster !== null); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public async $deleteClustersFrom(height: number): Promise<void> { |   public async $deleteClustersFrom(height: number): Promise<void> { | ||||||
|     logger.info(`Delete newer cpfp clusters from height ${height} from the database`); |     logger.info(`Delete newer cpfp clusters from height ${height} from the database`); | ||||||
|     try { |     try { | ||||||
| @ -122,6 +142,37 @@ class CpfpRepository { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $deleteClustersAt(height: number): Promise<void> { | ||||||
|  |     logger.info(`Delete cpfp clusters at height ${height} from the database`); | ||||||
|  |     try { | ||||||
|  |       const [rows] = await DB.query( | ||||||
|  |         ` | ||||||
|  |           SELECT txs, height, root from compact_cpfp_clusters | ||||||
|  |           WHERE height = ? | ||||||
|  |         `,
 | ||||||
|  |         [height] | ||||||
|  |       ) as RowDataPacket[][]; | ||||||
|  |       if (rows?.length) { | ||||||
|  |         for (const clusterToDelete of rows) { | ||||||
|  |           const txs = this.unpack(clusterToDelete?.txs); | ||||||
|  |           for (const tx of txs) { | ||||||
|  |             await transactionRepository.$removeTransaction(tx.txid); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       await DB.query( | ||||||
|  |         ` | ||||||
|  |           DELETE from compact_cpfp_clusters | ||||||
|  |           WHERE height = ? | ||||||
|  |         `,
 | ||||||
|  |         [height] | ||||||
|  |       ); | ||||||
|  |     } catch (e: any) { | ||||||
|  |       logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   // insert a dummy row to mark that we've indexed as far as this block
 |   // insert a dummy row to mark that we've indexed as far as this block
 | ||||||
|   public async $insertProgressMarker(height: number): Promise<void> { |   public async $insertProgressMarker(height: number): Promise<void> { | ||||||
|     try { |     try { | ||||||
| @ -190,6 +241,32 @@ class CpfpRepository { | |||||||
|       return []; |       return []; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   // returns `true` if two sets of CPFP clusters are deeply identical
 | ||||||
|  |   public compareClusters(clustersA: CpfpCluster[], clustersB: CpfpCluster[]): boolean { | ||||||
|  |     if (clustersA.length !== clustersB.length) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     clustersA = clustersA.sort((a,b) => a.root.localeCompare(b.root)); | ||||||
|  |     clustersB = clustersB.sort((a,b) => a.root.localeCompare(b.root)); | ||||||
|  | 
 | ||||||
|  |     for (let i = 0; i < clustersA.length; i++) { | ||||||
|  |       if (clustersA[i].root !== clustersB[i].root) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       if (clustersA[i].txs.length !== clustersB[i].txs.length) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       for (let j = 0; j < clustersA[i].txs.length; j++) { | ||||||
|  |         if (clustersA[i].txs[j].txid !== clustersB[i].txs[j].txid) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new CpfpRepository(); | export default new CpfpRepository(); | ||||||
| @ -68,7 +68,7 @@ export class BlockOverviewTooltipComponent implements OnChanges { | |||||||
|       this.effectiveRate = this.tx.rate; |       this.effectiveRate = this.tx.rate; | ||||||
|       const txFlags = BigInt(this.tx.flags) || 0n; |       const txFlags = BigInt(this.tx.flags) || 0n; | ||||||
|       this.acceleration = this.tx.acc || (txFlags & TransactionFlags.acceleration); |       this.acceleration = this.tx.acc || (txFlags & TransactionFlags.acceleration); | ||||||
|       this.hasEffectiveRate = this.tx.acc || Math.abs((this.fee / this.vsize) - this.effectiveRate) > 0.05 |       this.hasEffectiveRate = this.tx.acc || !(Math.abs((this.fee / this.vsize) - this.effectiveRate) <= 0.1 && Math.abs((this.fee / Math.ceil(this.vsize)) - this.effectiveRate) <= 0.1) | ||||||
|         || (txFlags && (txFlags & (TransactionFlags.cpfp_child | TransactionFlags.cpfp_parent)) > 0n); |         || (txFlags && (txFlags & (TransactionFlags.cpfp_child | TransactionFlags.cpfp_parent)) > 0n); | ||||||
|       this.filters = this.tx.flags ? toFilters(txFlags).filter(f => f.tooltip) : []; |       this.filters = this.tx.flags ? toFilters(txFlags).filter(f => f.tooltip) : []; | ||||||
|       this.activeFilters = {} |       this.activeFilters = {} | ||||||
|  | |||||||
| @ -599,7 +599,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|                 bestDescendant: tx.bestDescendant, |                 bestDescendant: tx.bestDescendant, | ||||||
|               }); |               }); | ||||||
|               const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant); |               const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant); | ||||||
|               this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01)); |               this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) >= 0.1)); | ||||||
|             } else { |             } else { | ||||||
|               this.fetchCpfp$.next(this.tx.txid); |               this.fetchCpfp$.next(this.tx.txid); | ||||||
|             } |             } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user