516 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			516 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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
 | |
|   );
 | |
| }
 |