mirror of
https://github.com/mempool/mempool.git
synced 2025-02-24 22:58:30 +01:00
Merge branch 'master' into nymkappa/apple-pay
This commit is contained in:
commit
29875e0095
32 changed files with 1111 additions and 925 deletions
|
@ -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<string, MempoolTx>,
|
||||
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<string, GraphTx> {
|
||||
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
|
||||
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
|
||||
for (const tx of transactions) {
|
||||
blockTxs.set(tx.txid, tx);
|
||||
for (const vin of tx.vin) {
|
||||
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
|
||||
}
|
||||
}
|
||||
|
||||
const relatives: Map<string, GraphTx> = new Map();
|
||||
const stack: string[] = [tx.txid];
|
||||
|
||||
// build set of same-block ancestors
|
||||
while (stack.length > 0) {
|
||||
const nextTxid = stack.pop();
|
||||
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
|
||||
if (!nextTx || relatives.has(nextTx.txid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mempoolTx = this.convertToGraphTx(nextTx);
|
||||
|
||||
mempoolTx.fees.base = nextTx.fee || 0;
|
||||
mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[];
|
||||
mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[];
|
||||
|
||||
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
|
||||
if (txid) {
|
||||
stack.push(txid);
|
||||
}
|
||||
}
|
||||
|
||||
relatives.set(mempoolTx.txid, mempoolTx);
|
||||
}
|
||||
|
||||
return relatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a raw transaction and converts it to MempoolTx format
|
||||
* fee and ancestor data is initialized with dummy/null values
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx {
|
||||
return {
|
||||
txid: tx.txid,
|
||||
vsize: Math.ceil(tx.weight / 4),
|
||||
weight: tx.weight,
|
||||
fees: {
|
||||
base: 0, // dummy
|
||||
},
|
||||
depends: [], // dummy
|
||||
spentby: [], //dummy
|
||||
};
|
||||
}
|
||||
|
||||
private convertGraphToMempoolTx(tx: GraphTx): MempoolTx {
|
||||
return {
|
||||
...tx,
|
||||
fees: {
|
||||
base: tx.fees.base,
|
||||
ancestor: tx.fees.base,
|
||||
},
|
||||
ancestorcount: 1,
|
||||
ancestorsize: Math.ceil(tx.weight / 4),
|
||||
ancestors: new Map<string, MempoolTx>(),
|
||||
ancestorRate: 0,
|
||||
individualRate: 0,
|
||||
score: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
|
||||
* Calculate the minimum set of transactions to fee-bump, their total vsize + fees
|
||||
*
|
||||
*
|
||||
* @param tx
|
||||
* @param ancestors
|
||||
*/
|
||||
private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map<string, MempoolTx>, targetFeeRate: number): AccelerationInfo {
|
||||
private calculateAccelerationAncestors(tx: GraphTx, relatives: Map<string, GraphTx>, targetFeeRate: number): AccelerationInfo {
|
||||
// add root tx to the ancestor map
|
||||
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<string, MempoolTx> | null = null;
|
||||
let includedInCluster: Map<string, GraphTx> | 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<string, MempoolTx>(best.ancestors?.entries() || []);
|
||||
const cluster = new Map<string, GraphTx>(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<string, MempoolTx>, visited: Map<string, Map<string, MempoolTx>>, depth: number = 0): Map<string, MempoolTx> {
|
||||
// sanity check for infinite recursion / too many ancestors (should never happen)
|
||||
if (depth >= 100) {
|
||||
logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`);
|
||||
throw new Error('invalid_tx_dependencies');
|
||||
}
|
||||
|
||||
// initialize the ancestor map for this tx
|
||||
tx.ancestors = new Map<string, MempoolTx>();
|
||||
tx.depends.forEach(parentId => {
|
||||
const parent = all.get(parentId);
|
||||
if (parent) {
|
||||
// add the parent
|
||||
tx.ancestors?.set(parentId, parent);
|
||||
// check for a cached copy of this parent's ancestors
|
||||
let ancestors = visited.get(parent.txid);
|
||||
if (!ancestors) {
|
||||
// recursively fetch the parent's ancestors
|
||||
ancestors = this.setAncestors(parent, all, visited, depth + 1);
|
||||
}
|
||||
// and add to this tx's map
|
||||
ancestors.forEach((ancestor, ancestorId) => {
|
||||
tx.ancestors?.set(ancestorId, ancestor);
|
||||
});
|
||||
}
|
||||
});
|
||||
visited.set(tx.txid, tx.ancestors);
|
||||
|
||||
return tx.ancestors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
||||
* by running setAncestors on each leaf, and caching intermediate results.
|
||||
* then initializes ancestor data for each transaction
|
||||
*
|
||||
* @param all
|
||||
*/
|
||||
private initializeRelatives(all: Map<string, GraphTx>): Map<string, MempoolTx> {
|
||||
const mempoolTxs = new Map<string, MempoolTx>();
|
||||
all.forEach(entry => {
|
||||
mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry));
|
||||
});
|
||||
const visited: Map<string, Map<string, MempoolTx>> = new Map();
|
||||
const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
||||
for (const leaf of leaves) {
|
||||
this.setAncestors(leaf, mempoolTxs, visited);
|
||||
}
|
||||
mempoolTxs.forEach(entry => {
|
||||
entry.ancestors?.forEach(ancestor => {
|
||||
entry.ancestorcount++;
|
||||
entry.ancestorsize += ancestor.vsize;
|
||||
entry.fees.ancestor += ancestor.fees.base;
|
||||
});
|
||||
this.setAncestorScores(entry);
|
||||
});
|
||||
return mempoolTxs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a cluster of transactions from an in-mempool dependency graph
|
||||
* and update the survivors' scores and ancestors
|
||||
*
|
||||
* @param cluster
|
||||
* @param ancestors
|
||||
*/
|
||||
private removeAncestors(cluster: Map<string, MempoolTx>, all: Map<string, MempoolTx>): void {
|
||||
// remove
|
||||
cluster.forEach(tx => {
|
||||
all.delete(tx.txid);
|
||||
});
|
||||
|
||||
// update survivors
|
||||
all.forEach(tx => {
|
||||
cluster.forEach(remove => {
|
||||
if (tx.ancestors?.has(remove.txid)) {
|
||||
// remove as dependency
|
||||
tx.ancestors.delete(remove.txid);
|
||||
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
||||
// update ancestor sizes and fees
|
||||
tx.ancestorsize -= remove.vsize;
|
||||
tx.fees.ancestor -= remove.fees.base;
|
||||
}
|
||||
});
|
||||
// recalculate fee rates
|
||||
this.setAncestorScores(tx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a mempool transaction, and set the fee rates and ancestor score
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
private setAncestorScores(tx: MempoolTx): void {
|
||||
tx.individualRate = tx.fees.base / tx.vsize;
|
||||
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
|
||||
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
||||
}
|
||||
|
||||
// Sort by descending score
|
||||
private mempoolComparator(a, b): number {
|
||||
return b.score - a.score;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationCosts;
|
||||
|
||||
interface TemplateTransaction {
|
||||
txid: string;
|
||||
order: number;
|
||||
weight: number;
|
||||
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
|
||||
sigops: number;
|
||||
fee: number;
|
||||
feeDelta: number;
|
||||
ancestors: string[];
|
||||
cluster: string[];
|
||||
effectiveFeePerVsize: number;
|
||||
}
|
||||
|
||||
interface MinerTransaction extends TemplateTransaction {
|
||||
inputs: string[];
|
||||
feePerVsize: number;
|
||||
relativesSet: boolean;
|
||||
ancestorMap: Map<string, MinerTransaction>;
|
||||
children: Set<MinerTransaction>;
|
||||
ancestorFee: number;
|
||||
ancestorVsize: number;
|
||||
ancestorSigops: number;
|
||||
score: number;
|
||||
used: boolean;
|
||||
modified: boolean;
|
||||
dependencyRate: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
|
||||
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
|
||||
*/
|
||||
export function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
|
||||
const auditPool: Map<string, MinerTransaction> = new Map();
|
||||
const mempoolArray: MinerTransaction[] = [];
|
||||
|
||||
candidates.forEach(tx => {
|
||||
// initializing everything up front helps V8 optimize property access later
|
||||
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
|
||||
const feePerVsize = (tx.fee / adjustedVsize);
|
||||
auditPool.set(tx.txid, {
|
||||
txid: tx.txid,
|
||||
order: txidToOrdering(tx.txid),
|
||||
fee: tx.fee,
|
||||
feeDelta: 0,
|
||||
weight: tx.weight,
|
||||
adjustedVsize,
|
||||
feePerVsize: feePerVsize,
|
||||
effectiveFeePerVsize: feePerVsize,
|
||||
dependencyRate: feePerVsize,
|
||||
sigops: tx.sigops || 0,
|
||||
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
|
||||
relativesSet: false,
|
||||
ancestors: [],
|
||||
cluster: [],
|
||||
ancestorMap: new Map<string, MinerTransaction>(),
|
||||
children: new Set<MinerTransaction>(),
|
||||
ancestorFee: 0,
|
||||
ancestorVsize: 0,
|
||||
ancestorSigops: 0,
|
||||
score: 0,
|
||||
used: false,
|
||||
modified: false,
|
||||
});
|
||||
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
|
||||
});
|
||||
|
||||
// set accelerated effective fee
|
||||
for (const acceleration of accelerations) {
|
||||
const tx = auditPool.get(acceleration.txid);
|
||||
if (tx) {
|
||||
tx.feeDelta = acceleration.max_bid;
|
||||
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
|
||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
||||
tx.dependencyRate = tx.feePerVsize;
|
||||
}
|
||||
}
|
||||
|
||||
// Build relatives graph & calculate ancestor scores
|
||||
for (const tx of mempoolArray) {
|
||||
if (!tx.relativesSet) {
|
||||
setRelatives(tx, auditPool);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by descending ancestor score
|
||||
mempoolArray.sort(priorityComparator);
|
||||
|
||||
// Build blocks by greedily choosing the highest feerate package
|
||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||
const blocks: number[][] = [];
|
||||
let blockWeight = 0;
|
||||
let blockSigops = 0;
|
||||
const transactions: MinerTransaction[] = [];
|
||||
let modified: MinerTransaction[] = [];
|
||||
const overflow: MinerTransaction[] = [];
|
||||
let failures = 0;
|
||||
while (mempoolArray.length || modified.length) {
|
||||
// skip invalid transactions
|
||||
while (mempoolArray[0].used || mempoolArray[0].modified) {
|
||||
mempoolArray.shift();
|
||||
}
|
||||
|
||||
// Select best next package
|
||||
let nextTx;
|
||||
const nextPoolTx = mempoolArray[0];
|
||||
const nextModifiedTx = modified[0];
|
||||
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
|
||||
nextTx = nextPoolTx;
|
||||
mempoolArray.shift();
|
||||
} else {
|
||||
modified.shift();
|
||||
if (nextModifiedTx) {
|
||||
nextTx = nextModifiedTx;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextTx && !nextTx?.used) {
|
||||
// Check if the package fits into this block
|
||||
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
|
||||
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||
const clusterTxids = sortedTxSet.map(tx => tx.txid);
|
||||
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
|
||||
const used: MinerTransaction[] = [];
|
||||
while (sortedTxSet.length) {
|
||||
const ancestor = sortedTxSet.pop();
|
||||
if (!ancestor) {
|
||||
continue;
|
||||
}
|
||||
ancestor.used = true;
|
||||
ancestor.usedBy = nextTx.txid;
|
||||
// update this tx with effective fee rate & relatives data
|
||||
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
|
||||
ancestor.effectiveFeePerVsize = effectiveFeeRate;
|
||||
}
|
||||
ancestor.cluster = clusterTxids;
|
||||
transactions.push(ancestor);
|
||||
blockWeight += ancestor.weight;
|
||||
blockSigops += ancestor.sigops;
|
||||
used.push(ancestor);
|
||||
}
|
||||
|
||||
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||
if (used.length) {
|
||||
used.forEach(tx => {
|
||||
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
|
||||
});
|
||||
}
|
||||
|
||||
failures = 0;
|
||||
} else {
|
||||
// hold this package in an overflow list while we check for smaller options
|
||||
overflow.push(nextTx);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
// this block is full
|
||||
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
|
||||
const queueEmpty = !mempoolArray.length && !modified.length;
|
||||
|
||||
if (exceededPackageTries || queueEmpty) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const tx of transactions) {
|
||||
tx.ancestors = Object.values(tx.ancestorMap);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
}
|
||||
|
||||
// traverse in-mempool ancestors
|
||||
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
||||
function setRelatives(
|
||||
tx: MinerTransaction,
|
||||
mempool: Map<string, MinerTransaction>,
|
||||
): void {
|
||||
for (const parent of tx.inputs) {
|
||||
const parentTx = mempool.get(parent);
|
||||
if (parentTx && !tx.ancestorMap?.has(parent)) {
|
||||
tx.ancestorMap.set(parent, parentTx);
|
||||
parentTx.children.add(tx);
|
||||
// visit each node only once
|
||||
if (!parentTx.relativesSet) {
|
||||
setRelatives(parentTx, mempool);
|
||||
}
|
||||
parentTx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorMap.set(ancestor.txid, ancestor);
|
||||
});
|
||||
}
|
||||
};
|
||||
tx.ancestorFee = (tx.fee + tx.feeDelta);
|
||||
tx.ancestorVsize = tx.adjustedVsize || 0;
|
||||
tx.ancestorSigops = tx.sigops || 0;
|
||||
tx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
|
||||
tx.ancestorVsize += ancestor.adjustedVsize;
|
||||
tx.ancestorSigops += ancestor.sigops;
|
||||
});
|
||||
tx.score = tx.ancestorFee / tx.ancestorVsize;
|
||||
tx.relativesSet = true;
|
||||
}
|
||||
|
||||
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
|
||||
// avoids recursion to limit call stack depth
|
||||
function updateDescendants(
|
||||
rootTx: MinerTransaction,
|
||||
mempool: Map<string, MinerTransaction>,
|
||||
modified: MinerTransaction[],
|
||||
clusterRate: number,
|
||||
): MinerTransaction[] {
|
||||
const descendantSet: Set<MinerTransaction> = new Set();
|
||||
// stack of nodes left to visit
|
||||
const descendants: MinerTransaction[] = [];
|
||||
let descendantTx: MinerTransaction | undefined;
|
||||
rootTx.children.forEach(childTx => {
|
||||
if (!descendantSet.has(childTx)) {
|
||||
descendants.push(childTx);
|
||||
descendantSet.add(childTx);
|
||||
}
|
||||
});
|
||||
while (descendants.length) {
|
||||
descendantTx = descendants.pop();
|
||||
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
||||
// remove tx as ancestor
|
||||
descendantTx.ancestorMap.delete(rootTx.txid);
|
||||
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
|
||||
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
|
||||
descendantTx.ancestorSigops -= rootTx.sigops;
|
||||
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
|
||||
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
|
||||
|
||||
if (!descendantTx.modified) {
|
||||
descendantTx.modified = true;
|
||||
modified.push(descendantTx);
|
||||
}
|
||||
|
||||
// add this node's children to the stack
|
||||
descendantTx.children.forEach(childTx => {
|
||||
// visit each node only once
|
||||
if (!descendantSet.has(childTx)) {
|
||||
descendants.push(childTx);
|
||||
descendantSet.add(childTx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// return new, resorted modified list
|
||||
return modified.sort(priorityComparator);
|
||||
}
|
||||
|
||||
// Used to sort an array of MinerTransactions by descending ancestor score
|
||||
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
|
||||
if (b.score === a.score) {
|
||||
// tie-break by txid for stability
|
||||
return a.order - b.order;
|
||||
} else {
|
||||
return b.score - a.score;
|
||||
}
|
||||
}
|
||||
|
||||
// returns the most significant 4 bytes of the txid as an integer
|
||||
function txidToOrdering(txid: string): number {
|
||||
return parseInt(
|
||||
txid.substring(62, 64) +
|
||||
txid.substring(60, 62) +
|
||||
txid.substring(58, 60) +
|
||||
txid.substring(56, 58),
|
||||
16
|
||||
);
|
||||
}
|
||||
export default new AccelerationCosts;
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<string, GraphTx>;
|
||||
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<string, string[]>();
|
||||
for (const tx of template) {
|
||||
const cluster = tx.cluster || [];
|
||||
const root = cluster.length ? cluster[cluster.length - 1] : null;
|
||||
if (cluster.length > 1 && root && !clusters.has(root)) {
|
||||
clusters.set(root, cluster);
|
||||
}
|
||||
txMap[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
|
||||
}
|
||||
|
||||
const clusterArray: CpfpCluster[] = [];
|
||||
|
||||
for (const cluster of clusters.values()) {
|
||||
for (const txid of cluster) {
|
||||
const mempoolTx = txMap[txid];
|
||||
if (mempoolTx) {
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
let matched = false;
|
||||
cluster.forEach(relativeTxid => {
|
||||
if (relativeTxid === txid) {
|
||||
matched = true;
|
||||
} else {
|
||||
const relative = {
|
||||
txid: relativeTxid,
|
||||
fee: txMap[relativeTxid].fee,
|
||||
weight: (txMap[relativeTxid].adjustedVsize * 4) || txMap[relativeTxid].weight,
|
||||
};
|
||||
if (matched) {
|
||||
descendants.push(relative);
|
||||
} else {
|
||||
ancestors.push(relative);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
Object.assign(mempoolTx, { ancestors, descendants, bestDescendant: null, cpfpChecked: true });
|
||||
}
|
||||
}
|
||||
const root = cluster[cluster.length - 1];
|
||||
clusterArray.push({
|
||||
root: root,
|
||||
height,
|
||||
txs: cluster.reverse().map(txid => ({
|
||||
txid,
|
||||
fee: txMap[txid].fee,
|
||||
weight: (txMap[txid].adjustedVsize * 4) || txMap[txid].weight,
|
||||
})),
|
||||
effectiveFeePerVsize: txMap[root].effectiveFeePerVsize,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
transactions: transactions.map(tx => txMap[tx.txid]),
|
||||
clusters: clusterArray,
|
||||
};
|
||||
ancestorcount: number;
|
||||
ancestorsize: number;
|
||||
ancestorRate: number;
|
||||
individualRate: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for
|
||||
* 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<string, GraphTx>();
|
||||
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<string, GraphTx>): Map<string, GraphTx> {
|
||||
const relatives: Map<string, GraphTx> = new Map();
|
||||
const stack: GraphTx[] = Array.from(ancestors.values());
|
||||
while (stack.length > 0) {
|
||||
if (relatives.size > MAX_GRAPH_SIZE) {
|
||||
return relatives;
|
||||
}
|
||||
|
||||
const nextTx = stack.pop();
|
||||
if (!nextTx) {
|
||||
continue;
|
||||
}
|
||||
relatives.set(nextTx.txid, nextTx);
|
||||
|
||||
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
|
||||
if (relatives.has(relativeTxid)) {
|
||||
// already processed this tx
|
||||
continue;
|
||||
}
|
||||
let mempoolTx = ancestors.get(relativeTxid);
|
||||
if (!mempoolTx && mempool[relativeTxid]) {
|
||||
mempoolTx = mempoolToGraphTx(mempool[relativeTxid]);
|
||||
}
|
||||
if (mempoolTx) {
|
||||
stack.push(mempoolTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
||||
* by running setAncestors on each leaf, and caching intermediate results.
|
||||
* then initializes ancestor data for each transaction
|
||||
*
|
||||
* @param all
|
||||
*/
|
||||
function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
|
||||
const visited: Map<string, Map<string, GraphTx>> = new Map();
|
||||
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
||||
for (const leaf of leaves) {
|
||||
setAncestors(leaf, mempoolTxs, visited);
|
||||
}
|
||||
mempoolTxs.forEach(entry => {
|
||||
entry.ancestorMap?.forEach(ancestor => {
|
||||
entry.ancestorcount++;
|
||||
entry.ancestorsize += ancestor.adjustedVsize;
|
||||
entry.fees.ancestor += ancestor.fees.base;
|
||||
});
|
||||
setAncestorScores(entry);
|
||||
});
|
||||
return mempoolTxs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a root transaction and a list of in-mempool ancestors,
|
||||
* Calculate the CPFP cluster
|
||||
|
@ -172,10 +240,10 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
|
|||
let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator);
|
||||
|
||||
// Iterate until we reach a cluster that includes our target tx
|
||||
let maxIterations = MAX_GRAPH_SIZE;
|
||||
let maxIterations = MAX_CLUSTER_ITERATIONS;
|
||||
let best = sortedRelatives.shift();
|
||||
let bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
|
||||
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestorMap.has(tx.txid)) && maxIterations > 0) {
|
||||
let bestCluster = new Map<string, GraphTx>(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<string, GraphTx>): Map<st
|
|||
// Grab the next highest scoring entry
|
||||
best = sortedRelatives.shift();
|
||||
if (best) {
|
||||
bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
|
||||
bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
|
||||
bestCluster.set(best?.txid, best);
|
||||
}
|
||||
}
|
||||
|
@ -199,88 +267,4 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
|
|||
bestCluster.set(tx.txid, tx);
|
||||
|
||||
return bestCluster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a cluster of transactions from an in-mempool dependency graph
|
||||
* and update the survivors' scores and ancestors
|
||||
*
|
||||
* @param cluster
|
||||
* @param ancestors
|
||||
*/
|
||||
function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
|
||||
// remove
|
||||
cluster.forEach(tx => {
|
||||
all.delete(tx.txid);
|
||||
});
|
||||
|
||||
// update survivors
|
||||
all.forEach(tx => {
|
||||
cluster.forEach(remove => {
|
||||
if (tx.ancestorMap?.has(remove.txid)) {
|
||||
// remove as dependency
|
||||
tx.ancestorMap.delete(remove.txid);
|
||||
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
||||
// update ancestor sizes and fees
|
||||
tx.ancestorsize -= remove.adjustedVsize;
|
||||
tx.fees.ancestor -= remove.fees.base;
|
||||
}
|
||||
});
|
||||
// recalculate fee rates
|
||||
setAncestorScores(tx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
|
||||
* for each transaction.
|
||||
*
|
||||
* @param tx
|
||||
* @param all
|
||||
*/
|
||||
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
|
||||
// sanity check for infinite recursion / too many ancestors (should never happen)
|
||||
if (depth > MAX_GRAPH_SIZE) {
|
||||
return tx.ancestorMap;
|
||||
}
|
||||
|
||||
// initialize the ancestor map for this tx
|
||||
tx.ancestorMap = new Map<string, GraphTx>();
|
||||
tx.depends.forEach(parentId => {
|
||||
const parent = all.get(parentId);
|
||||
if (parent) {
|
||||
// add the parent
|
||||
tx.ancestorMap?.set(parentId, parent);
|
||||
// check for a cached copy of this parent's ancestors
|
||||
let ancestors = visited.get(parent.txid);
|
||||
if (!ancestors) {
|
||||
// recursively fetch the parent's ancestors
|
||||
ancestors = setAncestors(parent, all, visited, depth + 1);
|
||||
}
|
||||
// and add to this tx's map
|
||||
ancestors.forEach((ancestor, ancestorId) => {
|
||||
tx.ancestorMap?.set(ancestorId, ancestor);
|
||||
});
|
||||
}
|
||||
});
|
||||
visited.set(tx.txid, tx.ancestorMap);
|
||||
|
||||
return tx.ancestorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a mempool transaction, and set the fee rates and ancestor score
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
function setAncestorScores(tx: GraphTx): GraphTx {
|
||||
tx.individualRate = (tx.fees.base * 100_000_000) / tx.adjustedVsize;
|
||||
tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / tx.ancestorsize;
|
||||
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Sort by descending score
|
||||
function mempoolComparator(a: GraphTx, b: GraphTx): number {
|
||||
return b.score - a.score;
|
||||
}
|
|
@ -396,10 +396,6 @@ class Mempool {
|
|||
}
|
||||
|
||||
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const changed: string[] = [];
|
||||
|
||||
|
|
515
backend/src/api/mini-miner.ts
Normal file
515
backend/src/api/mini-miner.ts
Normal file
|
@ -0,0 +1,515 @@
|
|||
import { Acceleration } from './acceleration/acceleration';
|
||||
import { MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
import logger from '../logger';
|
||||
|
||||
const BLOCK_WEIGHT_UNITS = 4_000_000;
|
||||
const BLOCK_SIGOPS = 80_000;
|
||||
const MAX_RELATIVE_GRAPH_SIZE = 100;
|
||||
|
||||
export interface GraphTx {
|
||||
txid: string;
|
||||
vsize: number;
|
||||
weight: number;
|
||||
depends: string[];
|
||||
spentby: string[];
|
||||
|
||||
ancestorcount: number;
|
||||
ancestorsize: number;
|
||||
fees: { // in sats
|
||||
base: number;
|
||||
ancestor: number;
|
||||
};
|
||||
|
||||
ancestors: Map<string, GraphTx>,
|
||||
ancestorRate: number;
|
||||
individualRate: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
interface TemplateTransaction {
|
||||
txid: string;
|
||||
order: number;
|
||||
weight: number;
|
||||
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
|
||||
sigops: number;
|
||||
fee: number;
|
||||
feeDelta: number;
|
||||
ancestors: string[];
|
||||
cluster: string[];
|
||||
effectiveFeePerVsize: number;
|
||||
}
|
||||
|
||||
interface MinerTransaction extends TemplateTransaction {
|
||||
inputs: string[];
|
||||
feePerVsize: number;
|
||||
relativesSet: boolean;
|
||||
ancestorMap: Map<string, MinerTransaction>;
|
||||
children: Set<MinerTransaction>;
|
||||
ancestorFee: number;
|
||||
ancestorVsize: number;
|
||||
ancestorSigops: number;
|
||||
score: number;
|
||||
used: boolean;
|
||||
modified: boolean;
|
||||
dependencyRate: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a raw transaction, and builds a graph of same-block relatives,
|
||||
* and returns as a GraphTx
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
export function getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
|
||||
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
|
||||
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
|
||||
for (const tx of transactions) {
|
||||
blockTxs.set(tx.txid, tx);
|
||||
for (const vin of tx.vin) {
|
||||
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
|
||||
}
|
||||
}
|
||||
|
||||
const relatives: Map<string, GraphTx> = new Map();
|
||||
const stack: string[] = [tx.txid];
|
||||
|
||||
// build set of same-block ancestors
|
||||
while (stack.length > 0) {
|
||||
const nextTxid = stack.pop();
|
||||
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
|
||||
if (!nextTx || relatives.has(nextTx.txid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mempoolTx = convertToGraphTx(nextTx, spendMap);
|
||||
|
||||
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
|
||||
if (txid) {
|
||||
stack.push(txid);
|
||||
}
|
||||
}
|
||||
|
||||
relatives.set(mempoolTx.txid, mempoolTx);
|
||||
}
|
||||
|
||||
return relatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a raw transaction and converts it to GraphTx format
|
||||
* fee and ancestor data is initialized with dummy/null values
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
export function convertToGraphTx(tx: MempoolTransactionExtended, spendMap?: Map<string, MempoolTransactionExtended | string>): GraphTx {
|
||||
return {
|
||||
txid: tx.txid,
|
||||
vsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)),
|
||||
weight: tx.weight,
|
||||
fees: {
|
||||
base: tx.fee || 0,
|
||||
ancestor: tx.fee || 0,
|
||||
},
|
||||
depends: (tx.vin.map(vin => vin.txid).filter(depend => depend) as string[]),
|
||||
spentby: spendMap ? (tx.vout.map((vout, index) => { const spend = spendMap.get(`${tx.txid}:${index}`); return (spend?.['txid'] || spend); }).filter(spent => spent) as string[]) : [],
|
||||
|
||||
ancestorcount: 1,
|
||||
ancestorsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)),
|
||||
ancestors: new Map<string, GraphTx>(),
|
||||
ancestorRate: 0,
|
||||
individualRate: 0,
|
||||
score: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives
|
||||
*/
|
||||
export function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>, spendMap: Map<string, MempoolTransactionExtended>): Map<string, GraphTx> {
|
||||
const relatives: Map<string, GraphTx> = new Map();
|
||||
const stack: GraphTx[] = Array.from(ancestors.values());
|
||||
while (stack.length > 0) {
|
||||
if (relatives.size > MAX_RELATIVE_GRAPH_SIZE) {
|
||||
return relatives;
|
||||
}
|
||||
|
||||
const nextTx = stack.pop();
|
||||
if (!nextTx) {
|
||||
continue;
|
||||
}
|
||||
relatives.set(nextTx.txid, nextTx);
|
||||
|
||||
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
|
||||
if (relatives.has(relativeTxid)) {
|
||||
// already processed this tx
|
||||
continue;
|
||||
}
|
||||
let ancestorTx = ancestors.get(relativeTxid);
|
||||
if (!ancestorTx && relativeTxid in mempool) {
|
||||
const mempoolTx = mempool[relativeTxid];
|
||||
ancestorTx = convertToGraphTx(mempoolTx, spendMap);
|
||||
}
|
||||
if (ancestorTx) {
|
||||
stack.push(ancestorTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
|
||||
* for each transaction.
|
||||
*
|
||||
* @param tx
|
||||
* @param all
|
||||
*/
|
||||
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
|
||||
// sanity check for infinite recursion / too many ancestors (should never happen)
|
||||
if (depth > MAX_RELATIVE_GRAPH_SIZE) {
|
||||
logger.warn('cpfp dependency calculation failed: setAncestors reached depth of 100, unable to proceed');
|
||||
return tx.ancestors;
|
||||
}
|
||||
|
||||
// initialize the ancestor map for this tx
|
||||
tx.ancestors = new Map<string, GraphTx>();
|
||||
tx.depends.forEach(parentId => {
|
||||
const parent = all.get(parentId);
|
||||
if (parent) {
|
||||
// add the parent
|
||||
tx.ancestors?.set(parentId, parent);
|
||||
// check for a cached copy of this parent's ancestors
|
||||
let ancestors = visited.get(parent.txid);
|
||||
if (!ancestors) {
|
||||
// recursively fetch the parent's ancestors
|
||||
ancestors = setAncestors(parent, all, visited, depth + 1);
|
||||
}
|
||||
// and add to this tx's map
|
||||
ancestors.forEach((ancestor, ancestorId) => {
|
||||
tx.ancestors?.set(ancestorId, ancestor);
|
||||
});
|
||||
}
|
||||
});
|
||||
visited.set(tx.txid, tx.ancestors);
|
||||
|
||||
return tx.ancestors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
||||
* by running setAncestors on each leaf, and caching intermediate results.
|
||||
* then initializes ancestor data for each transaction
|
||||
*
|
||||
* @param all
|
||||
*/
|
||||
export function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
|
||||
const visited: Map<string, Map<string, GraphTx>> = new Map();
|
||||
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
||||
for (const leaf of leaves) {
|
||||
setAncestors(leaf, mempoolTxs, visited);
|
||||
}
|
||||
mempoolTxs.forEach(entry => {
|
||||
entry.ancestors?.forEach(ancestor => {
|
||||
entry.ancestorcount++;
|
||||
entry.ancestorsize += ancestor.vsize;
|
||||
entry.fees.ancestor += ancestor.fees.base;
|
||||
});
|
||||
setAncestorScores(entry);
|
||||
});
|
||||
return mempoolTxs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a cluster of transactions from an in-mempool dependency graph
|
||||
* and update the survivors' scores and ancestors
|
||||
*
|
||||
* @param cluster
|
||||
* @param ancestors
|
||||
*/
|
||||
export function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
|
||||
// remove
|
||||
cluster.forEach(tx => {
|
||||
all.delete(tx.txid);
|
||||
});
|
||||
|
||||
// update survivors
|
||||
all.forEach(tx => {
|
||||
cluster.forEach(remove => {
|
||||
if (tx.ancestors?.has(remove.txid)) {
|
||||
// remove as dependency
|
||||
tx.ancestors.delete(remove.txid);
|
||||
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
||||
// update ancestor sizes and fees
|
||||
tx.ancestorsize -= remove.vsize;
|
||||
tx.fees.ancestor -= remove.fees.base;
|
||||
}
|
||||
});
|
||||
// recalculate fee rates
|
||||
setAncestorScores(tx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a mempool transaction, and set the fee rates and ancestor score
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
export function setAncestorScores(tx: GraphTx): void {
|
||||
tx.individualRate = tx.fees.base / tx.vsize;
|
||||
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
|
||||
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
||||
}
|
||||
|
||||
// Sort by descending score
|
||||
export function mempoolComparator(a: GraphTx, b: GraphTx): number {
|
||||
return b.score - a.score;
|
||||
}
|
||||
|
||||
/*
|
||||
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
|
||||
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
|
||||
*/
|
||||
export function makeBlockTemplate(candidates: MempoolTransactionExtended[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
|
||||
const auditPool: Map<string, MinerTransaction> = new Map();
|
||||
const mempoolArray: MinerTransaction[] = [];
|
||||
|
||||
candidates.forEach(tx => {
|
||||
// initializing everything up front helps V8 optimize property access later
|
||||
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
|
||||
const feePerVsize = (tx.fee / adjustedVsize);
|
||||
auditPool.set(tx.txid, {
|
||||
txid: tx.txid,
|
||||
order: txidToOrdering(tx.txid),
|
||||
fee: tx.fee,
|
||||
feeDelta: 0,
|
||||
weight: tx.weight,
|
||||
adjustedVsize,
|
||||
feePerVsize: feePerVsize,
|
||||
effectiveFeePerVsize: feePerVsize,
|
||||
dependencyRate: feePerVsize,
|
||||
sigops: tx.sigops || 0,
|
||||
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
|
||||
relativesSet: false,
|
||||
ancestors: [],
|
||||
cluster: [],
|
||||
ancestorMap: new Map<string, MinerTransaction>(),
|
||||
children: new Set<MinerTransaction>(),
|
||||
ancestorFee: 0,
|
||||
ancestorVsize: 0,
|
||||
ancestorSigops: 0,
|
||||
score: 0,
|
||||
used: false,
|
||||
modified: false,
|
||||
});
|
||||
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
|
||||
});
|
||||
|
||||
// set accelerated effective fee
|
||||
for (const acceleration of accelerations) {
|
||||
const tx = auditPool.get(acceleration.txid);
|
||||
if (tx) {
|
||||
tx.feeDelta = acceleration.max_bid;
|
||||
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
|
||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
||||
tx.dependencyRate = tx.feePerVsize;
|
||||
}
|
||||
}
|
||||
|
||||
// Build relatives graph & calculate ancestor scores
|
||||
for (const tx of mempoolArray) {
|
||||
if (!tx.relativesSet) {
|
||||
setRelatives(tx, auditPool);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by descending ancestor score
|
||||
mempoolArray.sort(priorityComparator);
|
||||
|
||||
// Build blocks by greedily choosing the highest feerate package
|
||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||
const blocks: number[][] = [];
|
||||
let blockWeight = 0;
|
||||
let blockSigops = 0;
|
||||
const transactions: MinerTransaction[] = [];
|
||||
let modified: MinerTransaction[] = [];
|
||||
const overflow: MinerTransaction[] = [];
|
||||
let failures = 0;
|
||||
while (mempoolArray.length || modified.length) {
|
||||
// skip invalid transactions
|
||||
while (mempoolArray[0].used || mempoolArray[0].modified) {
|
||||
mempoolArray.shift();
|
||||
}
|
||||
|
||||
// Select best next package
|
||||
let nextTx;
|
||||
const nextPoolTx = mempoolArray[0];
|
||||
const nextModifiedTx = modified[0];
|
||||
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
|
||||
nextTx = nextPoolTx;
|
||||
mempoolArray.shift();
|
||||
} else {
|
||||
modified.shift();
|
||||
if (nextModifiedTx) {
|
||||
nextTx = nextModifiedTx;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextTx && !nextTx?.used) {
|
||||
// Check if the package fits into this block
|
||||
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
|
||||
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||
const clusterTxids = sortedTxSet.map(tx => tx.txid);
|
||||
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
|
||||
const used: MinerTransaction[] = [];
|
||||
while (sortedTxSet.length) {
|
||||
const ancestor = sortedTxSet.pop();
|
||||
if (!ancestor) {
|
||||
continue;
|
||||
}
|
||||
ancestor.used = true;
|
||||
ancestor.usedBy = nextTx.txid;
|
||||
// update this tx with effective fee rate & relatives data
|
||||
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
|
||||
ancestor.effectiveFeePerVsize = effectiveFeeRate;
|
||||
}
|
||||
ancestor.cluster = clusterTxids;
|
||||
transactions.push(ancestor);
|
||||
blockWeight += ancestor.weight;
|
||||
blockSigops += ancestor.sigops;
|
||||
used.push(ancestor);
|
||||
}
|
||||
|
||||
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||
if (used.length) {
|
||||
used.forEach(tx => {
|
||||
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
|
||||
});
|
||||
}
|
||||
|
||||
failures = 0;
|
||||
} else {
|
||||
// hold this package in an overflow list while we check for smaller options
|
||||
overflow.push(nextTx);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
// this block is full
|
||||
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
|
||||
const queueEmpty = !mempoolArray.length && !modified.length;
|
||||
|
||||
if (exceededPackageTries || queueEmpty) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const tx of transactions) {
|
||||
tx.ancestors = Object.values(tx.ancestorMap);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
}
|
||||
|
||||
// traverse in-mempool ancestors
|
||||
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
||||
function setRelatives(
|
||||
tx: MinerTransaction,
|
||||
mempool: Map<string, MinerTransaction>,
|
||||
): void {
|
||||
for (const parent of tx.inputs) {
|
||||
const parentTx = mempool.get(parent);
|
||||
if (parentTx && !tx.ancestorMap?.has(parent)) {
|
||||
tx.ancestorMap.set(parent, parentTx);
|
||||
parentTx.children.add(tx);
|
||||
// visit each node only once
|
||||
if (!parentTx.relativesSet) {
|
||||
setRelatives(parentTx, mempool);
|
||||
}
|
||||
parentTx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorMap.set(ancestor.txid, ancestor);
|
||||
});
|
||||
}
|
||||
};
|
||||
tx.ancestorFee = (tx.fee + tx.feeDelta);
|
||||
tx.ancestorVsize = tx.adjustedVsize || 0;
|
||||
tx.ancestorSigops = tx.sigops || 0;
|
||||
tx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
|
||||
tx.ancestorVsize += ancestor.adjustedVsize;
|
||||
tx.ancestorSigops += ancestor.sigops;
|
||||
});
|
||||
tx.score = tx.ancestorFee / tx.ancestorVsize;
|
||||
tx.relativesSet = true;
|
||||
}
|
||||
|
||||
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
|
||||
// avoids recursion to limit call stack depth
|
||||
function updateDescendants(
|
||||
rootTx: MinerTransaction,
|
||||
mempool: Map<string, MinerTransaction>,
|
||||
modified: MinerTransaction[],
|
||||
clusterRate: number,
|
||||
): MinerTransaction[] {
|
||||
const descendantSet: Set<MinerTransaction> = new Set();
|
||||
// stack of nodes left to visit
|
||||
const descendants: MinerTransaction[] = [];
|
||||
let descendantTx: MinerTransaction | undefined;
|
||||
rootTx.children.forEach(childTx => {
|
||||
if (!descendantSet.has(childTx)) {
|
||||
descendants.push(childTx);
|
||||
descendantSet.add(childTx);
|
||||
}
|
||||
});
|
||||
while (descendants.length) {
|
||||
descendantTx = descendants.pop();
|
||||
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
||||
// remove tx as ancestor
|
||||
descendantTx.ancestorMap.delete(rootTx.txid);
|
||||
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
|
||||
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
|
||||
descendantTx.ancestorSigops -= rootTx.sigops;
|
||||
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
|
||||
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
|
||||
|
||||
if (!descendantTx.modified) {
|
||||
descendantTx.modified = true;
|
||||
modified.push(descendantTx);
|
||||
}
|
||||
|
||||
// add this node's children to the stack
|
||||
descendantTx.children.forEach(childTx => {
|
||||
// visit each node only once
|
||||
if (!descendantSet.has(childTx)) {
|
||||
descendants.push(childTx);
|
||||
descendantSet.add(childTx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// return new, resorted modified list
|
||||
return modified.sort(priorityComparator);
|
||||
}
|
||||
|
||||
// Used to sort an array of MinerTransactions by descending ancestor score
|
||||
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
|
||||
if (b.score === a.score) {
|
||||
// tie-break by txid for stability
|
||||
return a.order - b.order;
|
||||
} else {
|
||||
return b.score - a.score;
|
||||
}
|
||||
}
|
||||
|
||||
// returns the most significant 4 bytes of the txid as an integer
|
||||
function txidToOrdering(txid: string): number {
|
||||
return parseInt(
|
||||
txid.substring(62, 64) +
|
||||
txid.substring(60, 62) +
|
||||
txid.substring(58, 60) +
|
||||
txid.substring(56, 58),
|
||||
16
|
||||
);
|
||||
}
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
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();
|
||||
|
|
|
@ -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<Acceleration[] | null> {
|
||||
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<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
||||
|
||||
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<Acceleration[] | null> {
|
||||
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<Acceleration[] | null> {
|
||||
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<Acceleration[] | null> {
|
||||
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<string, Acceleration> = {};
|
||||
// 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<AccelerationHistory[] | null> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -223,6 +223,7 @@ export interface CpfpInfo {
|
|||
sigops?: number;
|
||||
adjustedVsize?: number,
|
||||
acceleration?: boolean,
|
||||
fee?: number;
|
||||
}
|
||||
|
||||
export interface TransactionStripped {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -91,6 +91,26 @@ class CpfpRepository {
|
|||
return;
|
||||
}
|
||||
|
||||
public async $getClustersAt(height: number): Promise<CpfpCluster[]> {
|
||||
const [clusterRows]: any = await DB.query(
|
||||
`
|
||||
SELECT *
|
||||
FROM compact_cpfp_clusters
|
||||
WHERE height = ?
|
||||
`,
|
||||
[height]
|
||||
);
|
||||
return clusterRows.map(cluster => {
|
||||
if (cluster?.txs) {
|
||||
cluster.effectiveFeePerVsize = cluster.fee_rate;
|
||||
cluster.txs = this.unpack(cluster.txs);
|
||||
return cluster;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).filter(cluster => cluster !== null);
|
||||
}
|
||||
|
||||
public async $deleteClustersFrom(height: number): Promise<void> {
|
||||
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<void> {
|
||||
logger.info(`Delete cpfp clusters at height ${height} from the database`);
|
||||
try {
|
||||
const [rows] = await DB.query(
|
||||
`
|
||||
SELECT txs, height, root from compact_cpfp_clusters
|
||||
WHERE height = ?
|
||||
`,
|
||||
[height]
|
||||
) as RowDataPacket[][];
|
||||
if (rows?.length) {
|
||||
for (const clusterToDelete of rows) {
|
||||
const txs = this.unpack(clusterToDelete?.txs);
|
||||
for (const tx of txs) {
|
||||
await transactionRepository.$removeTransaction(tx.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
await DB.query(
|
||||
`
|
||||
DELETE from compact_cpfp_clusters
|
||||
WHERE height = ?
|
||||
`,
|
||||
[height]
|
||||
);
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// insert a dummy row to mark that we've indexed as far as this block
|
||||
public async $insertProgressMarker(height: number): Promise<void> {
|
||||
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();
|
|
@ -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__
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -201,7 +201,7 @@
|
|||
<span i18n="address.error.loading-address-data">Error loading address data.</span>
|
||||
<br>
|
||||
<ng-container i18n="Electrum server limit exceeded error">
|
||||
<i>There many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i>
|
||||
<i>There are too many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i>
|
||||
<br><br>
|
||||
Consider viewing this address on the official Mempool website instead:
|
||||
</ng-container>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -8993,7 +8993,7 @@ export const restApiDocsData = [
|
|||
fragment: "accelerator-estimate",
|
||||
title: "POST Calculate Estimated Costs",
|
||||
description: {
|
||||
default: "<p>Returns estimated costs to accelerate a transaction. Optionally set the <code>api_key</code> header to get customized estimation.</p>"
|
||||
default: "<p>Returns estimated costs to accelerate a transaction. Optionally set the <code>X-Mempool-Auth</code> header to get customized estimation.</p>"
|
||||
},
|
||||
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,
|
||||
|
|
|
@ -536,6 +536,10 @@ export class ApiService {
|
|||
);
|
||||
}
|
||||
|
||||
logAccelerationRequest$(txid: string): Observable<any> {
|
||||
return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/acceleration/request/' + txid, '');
|
||||
}
|
||||
|
||||
// Cache methods
|
||||
async setBlockAuditLoaded(hash: string) {
|
||||
this.blockAuditLoaded[hash] = true;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1056,16 +1056,56 @@
|
|||
<context context-type="linenumber">91</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1bc4a5de56ea48a832e32294c124009867b478d0" datatype="html">
|
||||
<source>First seen</source>
|
||||
<trans-unit id="65d447765db0bf3390e9b3ecce142bf34bb602a3" datatype="html">
|
||||
<source>Mined</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration-timeline/acceleration-timeline.component.html</context>
|
||||
<context context-type="linenumber">26</context>
|
||||
<context context-type="linenumber">31</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration-timeline/acceleration-timeline.component.html</context>
|
||||
<context context-type="linenumber">120</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/custom-dashboard/custom-dashboard.component.html</context>
|
||||
<context context-type="linenumber">121</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/custom-dashboard/custom-dashboard.component.html</context>
|
||||
<context context-type="linenumber">154</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
|
||||
<context context-type="linenumber">183</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
|
||||
<context context-type="linenumber">245</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/rbf-list/rbf-list.component.html</context>
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html</context>
|
||||
<context context-type="linenumber">38</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
|
||||
<context context-type="linenumber">86</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
|
||||
<context context-type="linenumber">106</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">transaction.rbf.mined</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1bc4a5de56ea48a832e32294c124009867b478d0" datatype="html">
|
||||
<source>First seen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration-timeline/acceleration-timeline.component.html</context>
|
||||
<context context-type="linenumber">64</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block-overview-tooltip/block-overview-tooltip.component.html</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
|
@ -1125,11 +1165,11 @@
|
|||
<source>Accelerated</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration-timeline/acceleration-timeline.component.html</context>
|
||||
<context context-type="linenumber">40</context>
|
||||
<context context-type="linenumber">90</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration-timeline/acceleration-timeline.component.html</context>
|
||||
<context context-type="linenumber">136</context>
|
||||
<context context-type="linenumber">94</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block-overview-tooltip/block-overview-tooltip.component.html</context>
|
||||
|
@ -1149,50 +1189,6 @@
|
|||
</context-group>
|
||||
<note priority="1" from="description">transaction.audit.accelerated</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="65d447765db0bf3390e9b3ecce142bf34bb602a3" datatype="html">
|
||||
<source>Mined</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration-timeline/acceleration-timeline.component.html</context>
|
||||
<context context-type="linenumber">53</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration-timeline/acceleration-timeline.component.html</context>
|
||||
<context context-type="linenumber">93</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/custom-dashboard/custom-dashboard.component.html</context>
|
||||
<context context-type="linenumber">121</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/custom-dashboard/custom-dashboard.component.html</context>
|
||||
<context context-type="linenumber">154</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
|
||||
<context context-type="linenumber">183</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
|
||||
<context context-type="linenumber">245</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/rbf-list/rbf-list.component.html</context>
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html</context>
|
||||
<context context-type="linenumber">38</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
|
||||
<context context-type="linenumber">86</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
|
||||
<context context-type="linenumber">106</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">transaction.rbf.mined</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bcf34abc2d9ed8f45a2f65dd464c46694e9a181e" datatype="html">
|
||||
<source>Acceleration Fees</source>
|
||||
<context-group purpose="location">
|
||||
|
@ -1201,7 +1197,7 @@
|
|||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts</context>
|
||||
<context context-type="linenumber">74</context>
|
||||
<context context-type="linenumber">77</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/graphs/graphs.component.html</context>
|
||||
|
@ -1213,14 +1209,14 @@
|
|||
<source>No accelerated transaction for this timeframe</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts</context>
|
||||
<context context-type="linenumber">130</context>
|
||||
<context context-type="linenumber">133</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4793828002882320882" datatype="html">
|
||||
<source>At block: <x id="PH" equiv-text="ticks[0].data[2]"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts</context>
|
||||
<context context-type="linenumber">174</context>
|
||||
<context context-type="linenumber">177</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts</context>
|
||||
|
@ -1239,7 +1235,7 @@
|
|||
<source>Around block: <x id="PH" equiv-text="ticks[0].data[2]"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts</context>
|
||||
<context context-type="linenumber">176</context>
|
||||
<context context-type="linenumber">179</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts</context>
|
||||
|
@ -1669,7 +1665,7 @@
|
|||
<source>Accelerated by</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html</context>
|
||||
<context context-type="linenumber">25</context>
|
||||
<context context-type="linenumber">30</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Accelerated to hashrate</note>
|
||||
<note priority="1" from="meaning">transaction.accelerated-by-hashrate</note>
|
||||
|
@ -1678,7 +1674,7 @@
|
|||
<source><x id="INTERPOLATION" equiv-text="{{ acceleratedByPercentage }}"/> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="symbol hashrate-label">"/>of hashrate<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html</context>
|
||||
<context context-type="linenumber">27</context>
|
||||
<context context-type="linenumber">32</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">accelerator.x-of-hash-rate</note>
|
||||
</trans-unit>
|
||||
|
@ -1686,7 +1682,7 @@
|
|||
<source>not accelerating</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts</context>
|
||||
<context context-type="linenumber">83</context>
|
||||
<context context-type="linenumber">85</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3590f5c3ef2810f637316edb8aaa86b8e907f152" datatype="html">
|
||||
|
@ -2030,8 +2026,8 @@
|
|||
</context-group>
|
||||
<note priority="1" from="description">address.error.loading-address-data</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="49cef95661d86f4341788ce40068d58801adc6e6" datatype="html">
|
||||
<source><x id="START_ITALIC_TEXT" ctype="x-i" equiv-text="There many transactions on this address, more than your backend can handle. See more on <a href="/d"/>There many transactions on this address, more than your backend can handle. See more on <x id="START_LINK" ctype="x-a" equiv-text="<a href="/docs/faq#address-lookup-issues">"/>setting up a stronger backend<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/>.<x id="CLOSE_ITALIC_TEXT" ctype="x-i" equiv-text="</i>"/><x id="LINE_BREAK" ctype="lb"/><x id="LINE_BREAK" ctype="lb"/> Consider viewing this address on the official Mempool website instead: </source>
|
||||
<trans-unit id="9eb81e2576ffe4e8fb0a303e203040b6ab23cc22" datatype="html">
|
||||
<source><x id="START_ITALIC_TEXT" ctype="x-i" equiv-text="There are too many transactions on this address, more than your backend can handle. See more on <"/>There are too many transactions on this address, more than your backend can handle. See more on <x id="START_LINK" ctype="x-a" equiv-text="<a href="/docs/faq#address-lookup-issues">"/>setting up a stronger backend<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/>.<x id="CLOSE_ITALIC_TEXT" ctype="x-i" equiv-text="</i>"/><x id="LINE_BREAK" ctype="lb"/><x id="LINE_BREAK" ctype="lb"/> Consider viewing this address on the official Mempool website instead: </source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/address/address.component.html</context>
|
||||
<context context-type="linenumber">204,207</context>
|
||||
|
@ -6572,7 +6568,7 @@
|
|||
<source>Your transaction has been accelerated</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
|
||||
<context context-type="linenumber">141</context>
|
||||
<context context-type="linenumber">143</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">tracker.explain.accelerated</note>
|
||||
</trans-unit>
|
||||
|
@ -6580,7 +6576,7 @@
|
|||
<source>Waiting for your transaction to appear in the mempool</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
|
||||
<context context-type="linenumber">148</context>
|
||||
<context context-type="linenumber">150</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">tracker.explain.waiting</note>
|
||||
</trans-unit>
|
||||
|
@ -6588,7 +6584,7 @@
|
|||
<source>Your transaction is in the mempool, but it will not be confirmed for some time.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
|
||||
<context context-type="linenumber">154</context>
|
||||
<context context-type="linenumber">156</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">tracker.explain.pending</note>
|
||||
</trans-unit>
|
||||
|
@ -6596,7 +6592,7 @@
|
|||
<source>Your transaction is near the top of the mempool, and is expected to confirm soon.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
|
||||
<context context-type="linenumber">160</context>
|
||||
<context context-type="linenumber">162</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">tracker.explain.soon</note>
|
||||
</trans-unit>
|
||||
|
@ -6604,7 +6600,7 @@
|
|||
<source>Your transaction is expected to confirm in the next block</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
|
||||
<context context-type="linenumber">166</context>
|
||||
<context context-type="linenumber">168</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">tracker.explain.next-block</note>
|
||||
</trans-unit>
|
||||
|
@ -6612,7 +6608,7 @@
|
|||
<source>Your transaction is confirmed!</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
|
||||
<context context-type="linenumber">172</context>
|
||||
<context context-type="linenumber">174</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">tracker.explain.confirmed</note>
|
||||
</trans-unit>
|
||||
|
@ -6620,7 +6616,7 @@
|
|||
<source>Your transaction has been replaced by a newer version!</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
|
||||
<context context-type="linenumber">178</context>
|
||||
<context context-type="linenumber">180</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">tracker.explain.replaced</note>
|
||||
</trans-unit>
|
||||
|
@ -6628,7 +6624,7 @@
|
|||
<source>See more details</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
|
||||
<context context-type="linenumber">186</context>
|
||||
<context context-type="linenumber">189</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">accelerator.show-more-details</note>
|
||||
</trans-unit>
|
||||
|
@ -6644,7 +6640,7 @@
|
|||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/transaction/transaction.component.ts</context>
|
||||
<context context-type="linenumber">497</context>
|
||||
<context context-type="linenumber">498</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="meta.description.bitcoin.transaction" datatype="html">
|
||||
|
@ -6659,7 +6655,7 @@
|
|||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/transaction/transaction.component.ts</context>
|
||||
<context context-type="linenumber">501</context>
|
||||
<context context-type="linenumber">502</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7e06b8dd9f29261827018351cd71efe1c87839de" datatype="html">
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"ITEMS_PER_PAGE": 25,
|
||||
"LIGHTNING": true,
|
||||
"ACCELERATOR": true,
|
||||
"ACCELERATOR_BUTTON": true,
|
||||
"PUBLIC_ACCELERATIONS": true,
|
||||
"AUDIT": true
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ heat()
|
|||
|
||||
heatURLs=(
|
||||
'/api/v1/fees/recommended'
|
||||
'/api/v1/accelerations'
|
||||
)
|
||||
|
||||
while true
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue