diff --git a/backend/src/api/acceleration/acceleration.ts b/backend/src/api/acceleration/acceleration.ts index 2dbaa8b07..f26805ff2 100644 --- a/backend/src/api/acceleration/acceleration.ts +++ b/backend/src/api/acceleration/acceleration.ts @@ -1,15 +1,14 @@ import logger from '../../logger'; 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_SIGOPS = 80_000; const MAX_RELATIVE_GRAPH_SIZE = 200; const BID_BOOST_WINDOW = 40_000; const BID_BOOST_MIN_OFFSET = 10_000; const BID_BOOST_MAX_OFFSET = 400_000; -type Acceleration = { +export type Acceleration = { txid: string; max_bid: number; }; @@ -28,31 +27,6 @@ export interface AccelerationInfo { 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, - ancestorRate: number; - individualRate: number; - score: number; -} - class AccelerationCosts { /** * Takes a list of accelerations and verbose block data @@ -61,7 +35,7 @@ class AccelerationCosts { * @param accelerationsx * @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 // 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); @@ -170,108 +144,28 @@ class AccelerationCosts { /** * 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 - * - * @param txid - * @param medianFeeRate + * + * @param txid + * @param medianFeeRate */ public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo { // Get same-block transaction ancestors - const allRelatives = this.getSameBlockRelatives(tx, transactions); - const relativesMap = this.initializeRelatives(allRelatives); - const rootTx = relativesMap.get(tx.txid) as MempoolTx; + const allRelatives = getSameBlockRelatives(tx, transactions); + const relativesMap = initializeRelatives(allRelatives); + const rootTx = relativesMap.get(tx.txid) as GraphTx; // Calculate cost to boost 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 { - const blockTxs = new Map(); // map of txs in this block - const spendMap = new Map(); // 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 = 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(), - ancestorRate: 0, - individualRate: 0, - score: 0, - }; - } - /** * 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 - * + * * @param tx * @param ancestors */ - private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map, targetFeeRate: number): AccelerationInfo { + private calculateAccelerationAncestors(tx: GraphTx, relatives: Map, targetFeeRate: number): AccelerationInfo { // add root tx to the ancestor map relatives.set(tx.txid, tx); @@ -283,12 +177,12 @@ class AccelerationCosts { }); // Initialize individual & ancestor fee rates - relatives.forEach(entry => this.setAncestorScores(entry)); + relatives.forEach(entry => setAncestorScores(entry)); // 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 | null = null; + let includedInCluster: Map | null = null; // While highest score >= targetFeeRate let maxIterations = MAX_RELATIVE_GRAPH_SIZE; @@ -297,17 +191,17 @@ class AccelerationCosts { // Grab the highest scoring entry const best = sortedRelatives.shift(); if (best) { - const cluster = new Map(best.ancestors?.entries() || []); + const cluster = new Map(best.ancestors?.entries() || []); if (best.ancestors.has(tx.txid)) { includedInCluster = cluster; } cluster.set(best.txid, best); // 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 - this.removeAncestors(cluster, relatives); + removeAncestors(cluster, relatives); // 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), }; } - - /** - * 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, visited: Map>, depth: number = 0): Map { - // 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(); - 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): Map { - const mempoolTxs = new Map(); - all.forEach(entry => { - mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry)); - }); - const visited: Map> = 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, all: Map): 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; - -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; - children: Set; - 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 = 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(), - children: new Set(), - 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, -): 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, - modified: MinerTransaction[], - clusterRate: number, -): MinerTransaction[] { - const descendantSet: Set = 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 - ); -} +export default new AccelerationCosts; \ No newline at end of file diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 742ffe242..a65af3f19 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -19,7 +19,7 @@ import bitcoinClient from './bitcoin-client'; import difficultyAdjustment from '../difficulty-adjustment'; import transactionRepository from '../../repositories/TransactionRepository'; import rbfCache from '../rbf-cache'; -import { calculateCpfp } from '../cpfp'; +import { calculateMempoolTxCpfp } from '../cpfp'; class BitcoinRoutes { public initRoutes(app: Application) { @@ -160,6 +160,7 @@ class BitcoinRoutes { descendants: tx.descendants || null, effectiveFeePerVsize: tx.effectiveFeePerVsize || null, sigops: tx.sigops, + fee: tx.fee, adjustedVsize: tx.adjustedVsize, acceleration: tx.acceleration, acceleratedBy: tx.acceleratedBy || undefined, @@ -168,7 +169,7 @@ class BitcoinRoutes { return; } - const cpfpInfo = calculateCpfp(tx, mempool.getMempool()); + const cpfpInfo = calculateMempoolTxCpfp(tx, mempool.getMempool()); res.json(cpfpInfo); return; diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 9cc9233d5..762c81ff7 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -30,6 +30,9 @@ import redisCache from './redis-cache'; import rbfCache from './rbf-cache'; import { calcBitsDifference } from './difficulty-adjustment'; import AccelerationRepository from '../repositories/AccelerationRepository'; +import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; +import mempool from './mempool'; +import CpfpRepository from '../repositories/CpfpRepository'; class Blocks { private blocks: BlockExtended[] = []; @@ -567,8 +570,11 @@ class Blocks { const blockchainInfo = await bitcoinClient.getBlockchainInfo(); const currentBlockHeight = blockchainInfo.blocks; - const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesWithVersion(0); - const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesWithVersion(0); + const targetSummaryVersion: number = 1; + const targetTemplateVersion: number = 1; + + const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesBelowVersion(targetSummaryVersion); + const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesBelowVersion(targetTemplateVersion); // nothing to do if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) { @@ -601,16 +607,24 @@ class Blocks { for (let height = currentBlockHeight; height >= 0; height--) { try { - let txs: TransactionExtended[] | null = null; + let txs: MempoolTransactionExtended[] | null = null; if (unclassifiedBlocks[height]) { const blockHash = unclassifiedBlocks[height]; // fetch transactions - txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendTransaction(tx)) || []; + txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendMempoolTransaction(tx)) || []; // add CPFP - const cpfpSummary = Common.calculateCpfp(height, txs, true); + const cpfpSummary = calculateGoodBlockCpfp(height, txs, []); // classify 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); } if (unclassifiedTemplates[height]) { @@ -636,7 +650,7 @@ class Blocks { } 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 const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); 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 blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); @@ -1149,7 +1163,7 @@ class Blocks { transactions: cpfpSummary.transactions.map(tx => { let flags: number = 0; try { - flags = tx.flags || Common.getTransactionFlags(tx); + flags = Common.getTransactionFlags(tx); } catch (e) { logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); } @@ -1399,7 +1413,7 @@ class Blocks { } 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); diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 1d3b68541..cba39a511 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -419,12 +419,15 @@ export class Common { let flags = tx.flags ? BigInt(tx.flags) : 0n; // Update variable flags (CPFP, RBF) + flags &= ~TransactionFlags.cpfp_child; if (tx.ancestors?.length) { flags |= TransactionFlags.cpfp_child; } + flags &= ~TransactionFlags.cpfp_parent; if (tx.descendants?.length) { flags |= TransactionFlags.cpfp_parent; } + flags &= ~TransactionFlags.replacement; if (tx.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 { 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); diff --git a/backend/src/api/cpfp.ts b/backend/src/api/cpfp.ts index 604c1b3c9..5818eb1ea 100644 --- a/backend/src/api/cpfp.ts +++ b/backend/src/api/cpfp.ts @@ -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 { Acceleration } from './acceleration/acceleration'; 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 { - depends: string[]; - spentby: string[]; - ancestorMap: Map; - fees: { - base: number; - ancestor: number; +export function calculateFastBlockCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary { + const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block + const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster + let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster + let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root + const txMap: { [txid: string]: TransactionExtended } = {}; + // initialize the txMap + for (const tx of transactions) { + txMap[tx.txid] = tx; + } + // reverse pass to identify CPFP clusters + for (let i = transactions.length - 1; i >= 0; i--) { + const tx = transactions[i]; + if (!ancestors[tx.txid]) { + let totalFee = 0; + let totalVSize = 0; + clusterTxs.forEach(tx => { + totalFee += tx?.fee || 0; + totalVSize += (tx.weight / 4); + }); + const effectiveFeePerVsize = totalFee / totalVSize; + let cluster: CpfpCluster; + if (clusterTxs.length > 1) { + cluster = { + root: clusterTxs[0].txid, + height, + txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }), + effectiveFeePerVsize, + }; + clusters.push(cluster); + } + clusterTxs.forEach(tx => { + txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; + if (cluster) { + clusterMap[tx.txid] = cluster; + } + }); + // reset working vars + clusterTxs = []; + ancestors = {}; + } + clusterTxs.push(tx); + tx.vin.forEach(vin => { + ancestors[vin.txid] = true; + }); + } + // forward pass to enforce ancestor rate caps + for (const tx of transactions) { + let minAncestorRate = tx.effectiveFeePerVsize; + for (const vin of tx.vin) { + if (txMap[vin.txid]?.effectiveFeePerVsize) { + minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize); + } + } + // check rounded values to skip cases with almost identical fees + const roundedMinAncestorRate = Math.ceil(minAncestorRate); + const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize); + if (roundedMinAncestorRate < roundedEffectiveFeeRate) { + tx.effectiveFeePerVsize = minAncestorRate; + if (!clusterMap[tx.txid]) { + // add a single-tx cluster to record the dependent rate + const cluster = { + root: tx.txid, + height, + txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }], + effectiveFeePerVsize: minAncestorRate, + }; + clusterMap[tx.txid] = cluster; + clusters.push(cluster); + } else { + // update the existing cluster with the dependent rate + clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate; + } + } + } + if (saveRelatives) { + for (const cluster of clusters) { + cluster.txs.forEach((member, index) => { + txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse(); + txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse(); + txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize; + }); + } + } + return { + transactions, + clusters, + }; +} + +export function calculateGoodBlockCpfp(height: number, transactions: 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(); + 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 * 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)) { tx.cpfpDirty = false; return { @@ -32,30 +175,31 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: descendants: tx.descendants || [], effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize, sigops: tx.sigops, + fee: tx.fee, adjustedVsize: tx.adjustedVsize, acceleration: tx.acceleration }; } const ancestorMap = new Map(); - const graphTx = mempoolToGraphTx(tx); + const graphTx = convertToGraphTx(tx, memPool.getSpendMap()); ancestorMap.set(tx.txid, graphTx); - const allRelatives = expandRelativesGraph(mempool, ancestorMap); + const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap()); const relativesMap = initializeRelatives(allRelatives); const cluster = calculateCpfpCluster(tx.txid, relativesMap); let totalVsize = 0; let totalFee = 0; for (const tx of cluster.values()) { - totalVsize += tx.adjustedVsize; - totalFee += tx.fee; + totalVsize += tx.vsize; + totalFee += tx.fees.base; } const effectiveFeePerVsize = totalFee / totalVsize; for (const tx of cluster.values()) { mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; - mempool[tx.txid].ancestors = Array.from(tx.ancestorMap.values()).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.ancestorMap.has(entry.txid)).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.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); mempool[tx.txid].bestDescendant = null; mempool[tx.txid].cpfpChecked = true; mempool[tx.txid].cpfpDirty = true; @@ -70,88 +214,12 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: descendants: tx.descendants || [], effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize, sigops: tx.sigops, + fee: tx.fee, adjustedVsize: tx.adjustedVsize, acceleration: tx.acceleration }; } -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): Map { - const relatives: Map = 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): Map { - const visited: Map> = 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, * Calculate the CPFP cluster @@ -172,10 +240,10 @@ function calculateCpfpCluster(txid: string, graph: Map): Map(best?.ancestorMap?.entries() || []); - while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestorMap.has(tx.txid)) && maxIterations > 0) { + let bestCluster = new Map(best?.ancestors?.entries() || []); + while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestors.has(tx.txid)) && maxIterations > 0) { maxIterations--; if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) { break; @@ -190,7 +258,7 @@ function calculateCpfpCluster(txid: string, graph: Map): Map(best?.ancestorMap?.entries() || []); + bestCluster = new Map(best?.ancestors?.entries() || []); bestCluster.set(best?.txid, best); } } @@ -199,88 +267,4 @@ function calculateCpfpCluster(txid: string, graph: Map): Map, all: Map): 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, visited: Map>, depth: number = 0): Map { - // 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(); - 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; } \ No newline at end of file diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 89377335d..1f55179fb 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -396,10 +396,6 @@ class Mempool { } public $updateAccelerations(newAccelerations: Acceleration[]): string[] { - if (!config.MEMPOOL_SERVICES.ACCELERATIONS) { - return []; - } - try { const changed: string[] = []; diff --git a/backend/src/api/mini-miner.ts b/backend/src/api/mini-miner.ts new file mode 100644 index 000000000..4a4ef5daa --- /dev/null +++ b/backend/src/api/mini-miner.ts @@ -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, + 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; + children: Set; + 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 { + const blockTxs = new Map(); // map of txs in this block + const spendMap = new Map(); // 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 = 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): 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(), + 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, spendMap: Map): Map { + const relatives: Map = 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, visited: Map>, depth: number = 0): Map { + // 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(); + 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): Map { + const visited: Map> = 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, all: Map): 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 = 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(), + children: new Set(), + 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, +): 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, + modified: MinerTransaction[], + clusterRate: number, +): MinerTransaction[] { + const descendantSet: Set = 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 + ); +} diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index bdfc83d43..08ea0d1bc 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -9,6 +9,7 @@ import bitcoinClient from '../bitcoin/bitcoin-client'; import mining from "./mining"; import PricesRepository from '../../repositories/PricesRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository'; +import accelerationApi from '../services/acceleration'; class MiningRoutes { public initRoutes(app: Application) { @@ -41,6 +42,8 @@ class MiningRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations) .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals) + .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations', this.$getActiveAccelerations) + .post(config.MEMPOOL.API_URL_PREFIX + 'acceleration/request/:txid', this.$requestAcceleration) ; } @@ -445,6 +448,37 @@ class MiningRoutes { res.status(500).send(e instanceof Error ? e.message : e); } } + + private async $getActiveAccelerations(req: Request, res: Response): Promise { + try { + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { + res.status(400).send('Acceleration data is not available.'); + return; + } + res.status(200).send(accelerationApi.accelerations || []); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $requestAcceleration(req: Request, res: Response): Promise { + if (config.MEMPOOL_SERVICES.ACCELERATIONS || config.MEMPOOL.OFFICIAL) { + res.status(405).send('not available.'); + return; + } + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); + res.setHeader('expires', -1); + try { + accelerationApi.accelerationRequested(req.params.txid); + res.status(200).send('ok'); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } } export default new MiningRoutes(); diff --git a/backend/src/api/services/acceleration.ts b/backend/src/api/services/acceleration.ts index 7debe7119..386c40b8e 100644 --- a/backend/src/api/services/acceleration.ts +++ b/backend/src/api/services/acceleration.ts @@ -1,8 +1,10 @@ import config from '../../config'; import logger from '../../logger'; -import { BlockExtended, PoolTag } from '../../mempool.interfaces'; +import { BlockExtended } from '../../mempool.interfaces'; import axios from 'axios'; +type MyAccelerationStatus = 'requested' | 'accelerating' | 'done'; + export interface Acceleration { txid: string, added: number, @@ -35,18 +37,88 @@ export interface AccelerationHistory { }; class AccelerationApi { - public async $fetchAccelerations(): Promise { + private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations'); + private _accelerations: Acceleration[] | null = null; + private lastPoll = 0; + private forcePoll = false; + private myAccelerations: Record = {}; + + public get accelerations(): Acceleration[] | null { + return this._accelerations; + } + + public countMyAccelerationsWithStatus(filter: MyAccelerationStatus): number { + return Object.values(this.myAccelerations).reduce((count, {status}) => { return count + (status === filter ? 1 : 0); }, 0); + } + + public accelerationRequested(txid: string): void { + this.myAccelerations[txid] = { status: 'requested', added: Date.now() }; + } + + public accelerationConfirmed(): void { + this.forcePoll = true; + } + + private async $fetchAccelerations(): Promise { + try { + const response = await axios.get(this.apiPath, { responseType: 'json', timeout: 10000 }); + return response?.data || []; + } catch (e) { + logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e)); + return null; + } + } + + public async $updateAccelerations(): Promise { if (config.MEMPOOL_SERVICES.ACCELERATIONS) { - try { - const response = await axios.get(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`, { responseType: 'json', timeout: 10000 }); - return response.data as Acceleration[]; - } catch (e) { - logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e)); - return null; + const accelerations = await this.$fetchAccelerations(); + if (accelerations) { + this._accelerations = accelerations; + return this._accelerations; } } else { - return []; + return this.$updateAccelerationsOnDemand(); } + return null; + } + + private async $updateAccelerationsOnDemand(): Promise { + const shouldUpdate = this.forcePoll + || this.countMyAccelerationsWithStatus('requested') > 0 + || (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000))); + + // update accelerations if necessary + if (shouldUpdate) { + const accelerations = await this.$fetchAccelerations(); + this.lastPoll = Date.now(); + this.forcePoll = false; + if (accelerations) { + const latestAccelerations: Record = {}; + // set relevant accelerations to 'accelerating' + for (const acc of accelerations) { + if (this.myAccelerations[acc.txid]) { + latestAccelerations[acc.txid] = acc; + this.myAccelerations[acc.txid] = { status: 'accelerating', added: Date.now(), acceleration: acc }; + } + } + // txs that are no longer accelerating are either confirmed or canceled, so mark for expiry + for (const [txid, { status, acceleration }] of Object.entries(this.myAccelerations)) { + if (status === 'accelerating' && !latestAccelerations[txid]) { + this.myAccelerations[txid] = { status: 'done', added: Date.now(), acceleration }; + } + } + } + } + + // clear expired accelerations (confirmed / failed / not accepted) after 10 minutes + for (const [txid, { status, added }] of Object.entries(this.myAccelerations)) { + if (['requested', 'done'].includes(status) && added < (Date.now() - (1000 * 60 * 10))) { + delete this.myAccelerations[txid]; + } + } + + this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]; + return this._accelerations; } public async $fetchAccelerationHistory(page?: number, status?: string): Promise { diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index 9107f2ae7..b3077b935 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -103,7 +103,7 @@ class TransactionUtils { } const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4); const transactionExtended: TransactionExtended = Object.assign({ - vsize: Math.round(transaction.weight / 4), + vsize: transaction.weight / 4, feePerVsize: feePerVbytes, effectiveFeePerVsize: feePerVbytes, }, transaction); @@ -123,7 +123,7 @@ class TransactionUtils { const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize; const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, { order: this.txidToOrdering(transaction.txid), - vsize: Math.round(transaction.weight / 4), + vsize, adjustedVsize, sigops, feePerVsize: feePerVbytes, diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 8d1a85994..32d306ad2 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -33,7 +33,7 @@ interface AddressTransactions { removed: MempoolTransactionExtended[], } import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; -import { calculateCpfp } from './cpfp'; +import { calculateMempoolTxCpfp } from './cpfp'; // valid 'want' subscriptions const wantable = [ @@ -538,9 +538,9 @@ class WebsocketHandler { } if (config.MEMPOOL.RUST_GBT) { - await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, config.MEMPOOL_SERVICES.ACCELERATIONS); + await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, true); } else { - await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS); + await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, true); } const mBlocks = mempoolBlocks.getMempoolBlocks(); @@ -827,7 +827,7 @@ class WebsocketHandler { accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid), }; if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) { - calculateCpfp(mempoolTx, newMempool); + calculateMempoolTxCpfp(mempoolTx, newMempool); } if (mempoolTx.cpfpDirty) { positionData['cpfp'] = { @@ -866,7 +866,7 @@ class WebsocketHandler { acceleratedAt: mempoolTx.acceleratedAt || undefined, }; if (!mempoolTx.cpfpChecked) { - calculateCpfp(mempoolTx, newMempool); + calculateMempoolTxCpfp(mempoolTx, newMempool); } if (mempoolTx.cpfpDirty) { txInfo.cpfp = { @@ -949,18 +949,14 @@ class WebsocketHandler { if (config.MEMPOOL.AUDIT && memPool.isInSync()) { let projectedBlocks; const auditMempool = _memPool; - const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations())); + const isAccelerated = accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations())); - if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) { - if (config.MEMPOOL.RUST_GBT) { - const added = memPool.limitGBT ? (candidates?.added || []) : []; - const removed = memPool.limitGBT ? (candidates?.removed || []) : []; - projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id); - } else { - projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id); - } + if (config.MEMPOOL.RUST_GBT) { + const added = memPool.limitGBT ? (candidates?.added || []) : []; + const removed = memPool.limitGBT ? (candidates?.removed || []) : []; + projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id); } else { - projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); + projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id); } if (Common.indexingEnabled()) { @@ -1040,7 +1036,7 @@ class WebsocketHandler { const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions; await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true); } else { - await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, config.MEMPOOL_SERVICES.ACCELERATIONS); + await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, true); } const mBlocks = mempoolBlocks.getMempoolBlocks(); const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); diff --git a/backend/src/index.ts b/backend/src/index.ts index 2a1afc712..1d83c56a3 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -229,7 +229,7 @@ class Server { const newMempool = await bitcoinApi.$getRawMempool(); const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null; const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1; - const newAccelerations = await accelerationApi.$fetchAccelerations(); + const newAccelerations = await accelerationApi.$updateAccelerations(); const numHandledBlocks = await blocks.$updateBlocks(); const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1); if (numHandledBlocks === 0) { diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 5e8026d15..0ad60f4b9 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -223,6 +223,7 @@ export interface CpfpInfo { sigops?: number; adjustedVsize?: number, acceleration?: boolean, + fee?: number; } export interface TransactionStripped { diff --git a/backend/src/repositories/AccelerationRepository.ts b/backend/src/repositories/AccelerationRepository.ts index 0da66228c..70fa78dc6 100644 --- a/backend/src/repositories/AccelerationRepository.ts +++ b/backend/src/repositories/AccelerationRepository.ts @@ -1,4 +1,4 @@ -import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration'; +import { AccelerationInfo } from '../api/acceleration/acceleration'; import { RowDataPacket } from 'mysql2'; import DB from '../database'; import logger from '../logger'; @@ -11,6 +11,7 @@ import accelerationCosts from '../api/acceleration/acceleration'; import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; import transactionUtils from '../api/transaction-utils'; import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces'; +import { makeBlockTemplate } from '../api/mini-miner'; export interface PublicAcceleration { txid: string, @@ -212,6 +213,15 @@ class AccelerationRepository { this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations); } } + let anyConfirmed = false; + for (const acc of accelerations) { + if (blockTxs[acc.txid]) { + anyConfirmed = true; + } + } + if (anyConfirmed) { + accelerationApi.accelerationConfirmed(); + } const lastSyncedHeight = await this.$getLastSyncedHeight(); // if we've missed any blocks, let the indexer catch up from the last synced height on the next run if (block.height === lastSyncedHeight + 1) { diff --git a/backend/src/repositories/BlocksSummariesRepository.ts b/backend/src/repositories/BlocksSummariesRepository.ts index 63ad5ddf2..0268424f2 100644 --- a/backend/src/repositories/BlocksSummariesRepository.ts +++ b/backend/src/repositories/BlocksSummariesRepository.ts @@ -114,6 +114,43 @@ class BlocksSummariesRepository { 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 * diff --git a/backend/src/repositories/CpfpRepository.ts b/backend/src/repositories/CpfpRepository.ts index b33ff1e4a..0242188df 100644 --- a/backend/src/repositories/CpfpRepository.ts +++ b/backend/src/repositories/CpfpRepository.ts @@ -91,6 +91,26 @@ class CpfpRepository { return; } + public async $getClustersAt(height: number): Promise { + 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 { logger.info(`Delete newer cpfp clusters from height ${height} from the database`); try { @@ -122,6 +142,37 @@ class CpfpRepository { } } + public async $deleteClustersAt(height: number): Promise { + 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 public async $insertProgressMarker(height: number): Promise { try { @@ -190,6 +241,32 @@ class CpfpRepository { 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(); \ No newline at end of file diff --git a/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh index dc0fa6f7a..20b391087 100644 --- a/docker/frontend/entrypoint.sh +++ b/docker/frontend/entrypoint.sh @@ -40,6 +40,7 @@ __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0} __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0} __SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0} __ACCELERATOR__=${ACCELERATOR:=false} +__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true} __SERVICES_API__=${SERVICES_API:=false} __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} @@ -70,6 +71,7 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__ export __TESTNET_BLOCK_AUDIT_START_HEIGHT__ export __SIGNET_BLOCK_AUDIT_START_HEIGHT__ export __ACCELERATOR__ +export __ACCELERATOR_BUTTON__ export __SERVICES_API__ export __PUBLIC_ACCELERATIONS__ export __HISTORICAL_PRICE__ diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 4552a81f7..632a7b127 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs'; import { ServicesApiServices } from '../../services/services-api.service'; -import { md5, nextRoundNumber } from '../../shared/common.utils'; +import { md5, nextRoundNumber, insecureRandomUUID } from '../../shared/common.utils'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; import { ETA, EtaService } from '../../services/eta.service'; @@ -9,6 +9,7 @@ import { Transaction } from '../../interfaces/electrs.interface'; import { MiningStats } from '../../services/mining.service'; import { IAuth, AuthServiceMempool } from '../../services/auth.service'; import { EnterpriseService } from '../../services/enterprise.service'; +import { ApiService } from '../../services/api.service'; export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp'; @@ -125,6 +126,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { constructor( public stateService: StateService, + private apiService: ApiService, private servicesApiService: ServicesApiServices, private etaService: EtaService, private audioService: AudioService, @@ -132,7 +134,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { private authService: AuthServiceMempool, private enterpriseService: EnterpriseService, ) { - this.accelerationUUID = window.crypto.randomUUID(); + this.accelerationUUID = insecureRandomUUID(); // Check if Apple Pay available // @ts-ignore https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview @@ -384,10 +386,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); this.showSuccess = true; this.estimateSubscription.unsubscribe(); - this.moveToStep('paid') + this.moveToStep('paid'); }, error: (response) => { this.accelerateError = response.error; @@ -590,6 +593,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.cashAppPay) { this.cashAppPay.destroy(); @@ -639,9 +643,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } bitcoinPaymentCompleted(): void { + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); this.estimateSubscription.unsubscribe(); - this.moveToStep('paid') + this.moveToStep('paid'); } isLoggedIn(): boolean { diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 08eb841ee..76c64948b 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -201,7 +201,7 @@ Error loading address data.
- There many transactions on this address, more than your backend can handle. See more on setting up a stronger backend. + There are too many transactions on this address, more than your backend can handle. See more on setting up a stronger backend.

Consider viewing this address on the official Mempool website instead:
diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts index 79584b3d8..067061678 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { Subscription, of, timer } from 'rxjs'; -import { retry, switchMap, tap } from 'rxjs/operators'; +import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators'; import { ServicesApiServices } from '../../services/services-api.service'; @Component({ @@ -73,11 +73,11 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy { this.paymentStatus = 4; } this.paymentStatusSubscription = this.apiService.getPaymentStatus$(this.invoice.btcpayInvoiceId).pipe( - retry({ delay: () => timer(2000)}) - ).subscribe((response) => { - if (response.status === 204 || response.status === 404) { - return; - } + retry({ delay: () => timer(2000)}), + repeat({delay: 2000}), + filter((response) => response.status !== 204 && response.status !== 404), + take(1), + ).subscribe(() => { this.paymentStatus = 3; this.completed.emit(); }); diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts index d59bfa87f..0a606983e 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts @@ -68,7 +68,7 @@ export class BlockOverviewTooltipComponent implements OnChanges { this.effectiveRate = this.tx.rate; const txFlags = BigInt(this.tx.flags) || 0n; 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); this.filters = this.tx.flags ? toFilters(txFlags).filter(f => f.tooltip) : []; this.activeFilters = {} diff --git a/frontend/src/app/components/start/start.component.ts b/frontend/src/app/components/start/start.component.ts index 0a4943bde..78c31cde5 100644 --- a/frontend/src/app/components/start/start.component.ts +++ b/frontend/src/app/components/start/start.component.ts @@ -234,7 +234,7 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy { this.minScrollWidth = 40 + (8 * this.blockWidth) + (this.pageWidth * 2); if (firstVisibleBlock != null) { - this.scrollToBlock(firstVisibleBlock, offset); + this.scrollToBlock(firstVisibleBlock, offset + (this.isMobile ? this.blockWidth : 0)); } else { this.updatePages(); } diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index fef98d414..cbd6dd352 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -88,6 +88,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { blocksSubscription: Subscription; miningSubscription: Subscription; auditSubscription: Subscription; + txConfirmedSubscription: Subscription; currencyChangeSubscription: Subscription; fragmentParams: URLSearchParams; rbfTransaction: undefined | Transaction; @@ -141,7 +142,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { taprootEnabled: boolean; hasEffectiveFeeRate: boolean; accelerateCtaType: 'alert' | 'button' = 'button'; - acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === ''; + acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR_BUTTON && this.stateService.network === ''; eligibleForAcceleration: boolean = false; forceAccelerationSummary = false; hideAccelerationSummary = false; @@ -195,7 +196,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.stateService.networkChanged$.subscribe( (network) => { this.network = network; - this.acceleratorAvailable = this.stateService.env.ACCELERATOR && this.stateService.network === ''; + this.acceleratorAvailable = this.stateService.env.ACCELERATOR_BUTTON && this.stateService.network === ''; } ); @@ -599,7 +600,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { bestDescendant: 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 { this.fetchCpfp$.next(this.tx.txid); } @@ -625,7 +626,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } ); - this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => { + this.txConfirmedSubscription = this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => { if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) { if (this.tx.acceleration) { this.waitingForAccelerationInfo = true; @@ -1070,6 +1071,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.blocksSubscription.unsubscribe(); this.miningSubscription?.unsubscribe(); this.auditSubscription?.unsubscribe(); + this.txConfirmedSubscription?.unsubscribe(); this.currencyChangeSubscription?.unsubscribe(); this.leaveTransaction(); } diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 62f3c54c8..12bb96166 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -8993,7 +8993,7 @@ export const restApiDocsData = [ fragment: "accelerator-estimate", title: "POST Calculate Estimated Costs", description: { - default: "

Returns estimated costs to accelerate a transaction. Optionally set the api_key header to get customized estimation.

" + default: "

Returns estimated costs to accelerate a transaction. Optionally set the X-Mempool-Auth header to get customized estimation.

" }, urlString: "/v1/services/accelerator/estimate", showConditions: [""], @@ -9009,7 +9009,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29"], - headers: "api_key: stacksats", + headers: "X-Mempool-Auth: stacksats", response: `{ "txSummary": { "txid": "ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29", @@ -9240,7 +9240,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: [], - headers: "api_key: stacksats", + headers: "X-Mempool-Auth: stacksats", response: `[ { "type": "Bitcoin", @@ -9288,7 +9288,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: [], - headers: "api_key: stacksats", + headers: "X-Mempool-Auth: stacksats", response: `{ "balance": 99900000, "hold": 101829, @@ -9322,7 +9322,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29&userBid=21000000"], - headers: "api_key: stacksats", + headers: "X-Mempool-Auth: stacksats", response: `HTTP/1.1 200 OK`, }, } @@ -9352,7 +9352,7 @@ export const restApiDocsData = [ esModule: [], commonJS: [], curl: [], - headers: "api_key: stacksats", + headers: "X-Mempool-Auth: stacksats", response: `[ { "id": 89, diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index d7efa4d02..fa52ec707 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -536,6 +536,10 @@ export class ApiService { ); } + logAccelerationRequest$(txid: string): Observable { + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/acceleration/request/' + txid, ''); + } + // Cache methods async setBlockAuditLoaded(hash: string) { this.blockAuditLoaded[hash] = true; diff --git a/frontend/src/app/services/enterprise.service.ts b/frontend/src/app/services/enterprise.service.ts index 4ea890f1f..f9549cc8a 100644 --- a/frontend/src/app/services/enterprise.service.ts +++ b/frontend/src/app/services/enterprise.service.ts @@ -30,6 +30,7 @@ export class EnterpriseService { this.fetchSubdomainInfo(); this.disableSubnetworks(); this.stateService.env.ACCELERATOR = false; + this.stateService.env.ACCELERATOR_BUTTON = false; } else { this.insertMatomo(); } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index ef13ea07d..05f1ac69f 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -71,6 +71,7 @@ export interface Env { SIGNET_BLOCK_AUDIT_START_HEIGHT: number; HISTORICAL_PRICE: boolean; ACCELERATOR: boolean; + ACCELERATOR_BUTTON: boolean; PUBLIC_ACCELERATIONS: boolean; ADDITIONAL_CURRENCIES: boolean; GIT_COMMIT_HASH_MEMPOOL_SPACE?: string; @@ -108,6 +109,7 @@ const defaultEnv: Env = { 'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0, 'HISTORICAL_PRICE': true, 'ACCELERATOR': false, + 'ACCELERATOR_BUTTON': true, 'PUBLIC_ACCELERATIONS': false, 'ADDITIONAL_CURRENCIES': false, 'SERVICES_API': 'https://mempool.space/api/v1/services', diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 88ed5d8a9..697b11b5e 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -183,6 +183,19 @@ export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): Mempo }; } +export function insecureRandomUUID(): string { + const hexDigits = '0123456789abcdef'; + const uuidLengths = [8, 4, 4, 4, 12]; + let uuid = ''; + for (const length of uuidLengths) { + for (let i = 0; i < length; i++) { + uuid += hexDigits[Math.floor(Math.random() * 16)]; + } + uuid += '-'; + } + return uuid.slice(0, -1); +} + // https://stackoverflow.com/a/60467595 export function md5(inputString): string { var hc="0123456789abcdef"; @@ -225,4 +238,4 @@ export function md5(inputString): string { b=ii(b,c,d,a,x[i+ 9],21, -343485551);a=ad(a,olda);b=ad(b,oldb);c=ad(c,oldc);d=ad(d,oldd); } return rh(a)+rh(b)+rh(c)+rh(d); -} \ No newline at end of file +} diff --git a/frontend/src/locale/messages.xlf b/frontend/src/locale/messages.xlf index 60113a7ed..0f9432320 100644 --- a/frontend/src/locale/messages.xlf +++ b/frontend/src/locale/messages.xlf @@ -1056,16 +1056,56 @@ 91 - - First seen + + Mined src/app/components/acceleration-timeline/acceleration-timeline.component.html - 26 + 31 src/app/components/acceleration-timeline/acceleration-timeline.component.html 120 + + src/app/components/custom-dashboard/custom-dashboard.component.html + 121 + + + src/app/components/custom-dashboard/custom-dashboard.component.html + 154 + + + src/app/components/pool/pool.component.html + 183 + + + src/app/components/pool/pool.component.html + 245 + + + src/app/components/rbf-list/rbf-list.component.html + 23 + + + src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html + 38 + + + src/app/dashboard/dashboard.component.html + 86 + + + src/app/dashboard/dashboard.component.html + 106 + + transaction.rbf.mined + + + First seen + + src/app/components/acceleration-timeline/acceleration-timeline.component.html + 64 + src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 20 @@ -1125,11 +1165,11 @@ Accelerated src/app/components/acceleration-timeline/acceleration-timeline.component.html - 40 + 90 src/app/components/acceleration-timeline/acceleration-timeline.component.html - 136 + 94 src/app/components/block-overview-tooltip/block-overview-tooltip.component.html @@ -1149,50 +1189,6 @@ transaction.audit.accelerated - - Mined - - src/app/components/acceleration-timeline/acceleration-timeline.component.html - 53 - - - src/app/components/acceleration-timeline/acceleration-timeline.component.html - 93 - - - src/app/components/custom-dashboard/custom-dashboard.component.html - 121 - - - src/app/components/custom-dashboard/custom-dashboard.component.html - 154 - - - src/app/components/pool/pool.component.html - 183 - - - src/app/components/pool/pool.component.html - 245 - - - src/app/components/rbf-list/rbf-list.component.html - 23 - - - src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html - 38 - - - src/app/dashboard/dashboard.component.html - 86 - - - src/app/dashboard/dashboard.component.html - 106 - - transaction.rbf.mined - Acceleration Fees @@ -1201,7 +1197,7 @@ src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts - 74 + 77 src/app/components/graphs/graphs.component.html @@ -1213,14 +1209,14 @@ No accelerated transaction for this timeframe src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts - 130 + 133 At block: src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts - 174 + 177 src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts @@ -1239,7 +1235,7 @@ Around block: src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts - 176 + 179 src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts @@ -1669,7 +1665,7 @@ Accelerated by src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html - 25 + 30 Accelerated to hashrate transaction.accelerated-by-hashrate @@ -1678,7 +1674,7 @@ of hashrate src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html - 27 + 32 accelerator.x-of-hash-rate @@ -1686,7 +1682,7 @@ not accelerating src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts - 83 + 85 @@ -2030,8 +2026,8 @@ address.error.loading-address-data - - There many transactions on this address, more than your backend can handle. See more on setting up a stronger backend. Consider viewing this address on the official Mempool website instead: + + There are too many transactions on this address, more than your backend can handle. See more on setting up a stronger backend. Consider viewing this address on the official Mempool website instead: src/app/components/address/address.component.html 204,207 @@ -6572,7 +6568,7 @@ Your transaction has been accelerated src/app/components/tracker/tracker.component.html - 141 + 143 tracker.explain.accelerated @@ -6580,7 +6576,7 @@ Waiting for your transaction to appear in the mempool src/app/components/tracker/tracker.component.html - 148 + 150 tracker.explain.waiting @@ -6588,7 +6584,7 @@ Your transaction is in the mempool, but it will not be confirmed for some time. src/app/components/tracker/tracker.component.html - 154 + 156 tracker.explain.pending @@ -6596,7 +6592,7 @@ Your transaction is near the top of the mempool, and is expected to confirm soon. src/app/components/tracker/tracker.component.html - 160 + 162 tracker.explain.soon @@ -6604,7 +6600,7 @@ Your transaction is expected to confirm in the next block src/app/components/tracker/tracker.component.html - 166 + 168 tracker.explain.next-block @@ -6612,7 +6608,7 @@ Your transaction is confirmed! src/app/components/tracker/tracker.component.html - 172 + 174 tracker.explain.confirmed @@ -6620,7 +6616,7 @@ Your transaction has been replaced by a newer version! src/app/components/tracker/tracker.component.html - 178 + 180 tracker.explain.replaced @@ -6628,7 +6624,7 @@ See more details src/app/components/tracker/tracker.component.html - 186 + 189 accelerator.show-more-details @@ -6644,7 +6640,7 @@ src/app/components/transaction/transaction.component.ts - 497 + 498 @@ -6659,7 +6655,7 @@ src/app/components/transaction/transaction.component.ts - 501 + 502 diff --git a/production/mempool-frontend-config.mainnet.json b/production/mempool-frontend-config.mainnet.json index 0465cb7d3..84cde82cf 100644 --- a/production/mempool-frontend-config.mainnet.json +++ b/production/mempool-frontend-config.mainnet.json @@ -13,6 +13,7 @@ "ITEMS_PER_PAGE": 25, "LIGHTNING": true, "ACCELERATOR": true, + "ACCELERATOR_BUTTON": true, "PUBLIC_ACCELERATIONS": true, "AUDIT": true } diff --git a/production/nginx-cache-heater b/production/nginx-cache-heater index 4bbe8ee15..24ec8a061 100755 --- a/production/nginx-cache-heater +++ b/production/nginx-cache-heater @@ -9,6 +9,7 @@ heat() heatURLs=( '/api/v1/fees/recommended' + '/api/v1/accelerations' ) while true diff --git a/production/nginx/location-api-v1-services.conf b/production/nginx/location-api-v1-services.conf index 88f510e79..aad13264c 100644 --- a/production/nginx/location-api-v1-services.conf +++ b/production/nginx/location-api-v1-services.conf @@ -2,6 +2,9 @@ # routing # ########### +location /api/v1/accelerations { + try_files /dev/null @mempool-api-v1-services-cache-short; +} location /api/v1/assets { try_files /dev/null @mempool-api-v1-services-cache-short; }