Optimize makeBlockTemplates
This commit is contained in:
parent
832ccdac46
commit
968d7b827b
@ -1,3 +1,4 @@
|
|||||||
|
import logger from '../logger';
|
||||||
import { BlockExtended, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
import { BlockExtended, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||||
|
|
||||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, TransactionSet, Ancestor } from '../mempool.interfaces';
|
import { MempoolBlock, TransactionExtended, AuditTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { PairingHeap } from '../utils/pairing-heap';
|
||||||
|
|
||||||
class MempoolBlocks {
|
class MempoolBlocks {
|
||||||
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||||
@ -72,6 +73,7 @@ class MempoolBlocks {
|
|||||||
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
|
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
|
||||||
|
|
||||||
const { blocks, deltas } = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
|
const { blocks, deltas } = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
|
||||||
|
|
||||||
this.mempoolBlocks = blocks;
|
this.mempoolBlocks = blocks;
|
||||||
this.mempoolBlockDeltas = deltas;
|
this.mempoolBlockDeltas = deltas;
|
||||||
}
|
}
|
||||||
@ -144,226 +146,273 @@ class MempoolBlocks {
|
|||||||
* Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core
|
* Build projected mempool blocks 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)
|
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
|
||||||
*
|
*
|
||||||
* templateLimit: number of blocks to build using the full algo,
|
* blockLimit: number of blocks to build in total.
|
||||||
* remaining blocks up to blockLimit will skip the expensive updateDescendants step
|
* weightLimit: maximum weight of transactions to consider using the selection algorithm.
|
||||||
*
|
* if weightLimit is significantly lower than the mempool size, results may start to diverge from getBlockTemplate
|
||||||
* blockLimit: number of blocks to build in total. Excess transactions will be ignored.
|
* condenseRest: whether to ignore excess transactions or append them to the final block.
|
||||||
*/
|
*/
|
||||||
public makeBlockTemplates(mempool: { [txid: string]: TransactionExtended }, templateLimit: number = Infinity, blockLimit: number = Infinity): MempoolBlockWithTransactions[] {
|
public makeBlockTemplates(mempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit: number | null = null, condenseRest = false): MempoolBlockWithTransactions[] {
|
||||||
const start = new Date().getTime();
|
const start = Date.now();
|
||||||
const txSets: { [txid: string]: TransactionSet } = {};
|
const auditPool: { [txid: string]: AuditTransaction } = {};
|
||||||
const mempoolArray: TransactionExtended[] = Object.values(mempool);
|
const mempoolArray: AuditTransaction[] = [];
|
||||||
|
const restOfArray: TransactionExtended[] = [];
|
||||||
mempoolArray.forEach((tx) => {
|
|
||||||
tx.bestDescendant = null;
|
let weight = 0;
|
||||||
tx.ancestors = [];
|
const maxWeight = weightLimit ? Math.max(4_000_000 * blockLimit, weightLimit) : Infinity;
|
||||||
tx.cpfpChecked = false;
|
// grab the top feerate txs up to maxWeight
|
||||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => {
|
||||||
txSets[tx.txid] = {
|
weight += tx.weight;
|
||||||
fee: 0,
|
if (weight >= maxWeight) {
|
||||||
weight: 1,
|
restOfArray.push(tx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// initializing everything up front helps V8 optimize property access later
|
||||||
|
auditPool[tx.txid] = {
|
||||||
|
txid: tx.txid,
|
||||||
|
fee: tx.fee,
|
||||||
|
size: tx.size,
|
||||||
|
weight: tx.weight,
|
||||||
|
feePerVsize: tx.feePerVsize,
|
||||||
|
vin: tx.vin,
|
||||||
|
relativesSet: false,
|
||||||
|
ancestorMap: new Map<string, AuditTransaction>(),
|
||||||
|
children: new Set<AuditTransaction>(),
|
||||||
|
ancestorFee: 0,
|
||||||
|
ancestorWeight: 0,
|
||||||
score: 0,
|
score: 0,
|
||||||
children: [],
|
used: false,
|
||||||
available: true,
|
|
||||||
modified: false,
|
modified: false,
|
||||||
};
|
modifiedNode: null,
|
||||||
});
|
}
|
||||||
|
mempoolArray.push(auditPool[tx.txid]);
|
||||||
|
})
|
||||||
|
|
||||||
// Build relatives graph & calculate ancestor scores
|
// Build relatives graph & calculate ancestor scores
|
||||||
mempoolArray.forEach((tx) => {
|
for (const tx of mempoolArray) {
|
||||||
this.setRelatives(tx, mempool, txSets);
|
if (!tx.relativesSet) {
|
||||||
});
|
this.setRelatives(tx, auditPool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sort by descending ancestor score
|
// Sort by descending ancestor score
|
||||||
const byAncestor = (a, b): number => this.sortByAncestorScore(a, b, txSets);
|
mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0));
|
||||||
mempoolArray.sort(byAncestor);
|
|
||||||
|
|
||||||
// Build blocks by greedily choosing the highest feerate package
|
// Build blocks by greedily choosing the highest feerate package
|
||||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||||
const blocks: MempoolBlockWithTransactions[] = [];
|
const blocks: MempoolBlockWithTransactions[] = [];
|
||||||
let blockWeight = 4000;
|
let blockWeight = 4000;
|
||||||
let blockSize = 0;
|
let blockSize = 0;
|
||||||
let transactions: TransactionExtended[] = [];
|
let transactions: AuditTransaction[] = [];
|
||||||
let modified: TransactionExtended[] = [];
|
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0));
|
||||||
let overflow: TransactionExtended[] = [];
|
let overflow: AuditTransaction[] = [];
|
||||||
let failures = 0;
|
let failures = 0;
|
||||||
while ((mempoolArray.length || modified.length) && blocks.length < blockLimit) {
|
let top = 0;
|
||||||
const simpleMode = blocks.length >= templateLimit;
|
while ((top < mempoolArray.length || !modified.isEmpty()) && (condenseRest || blocks.length < blockLimit)) {
|
||||||
let anyModified = false;
|
// skip invalid transactions
|
||||||
// Select best next package
|
while (top < mempoolArray.length && (mempoolArray[top].used || mempoolArray[top].modified)) {
|
||||||
let nextTx;
|
top++;
|
||||||
if (mempoolArray.length && (!modified.length || txSets[mempoolArray[0].txid]?.score > txSets[modified[0].txid]?.score)) {
|
|
||||||
nextTx = mempoolArray.shift();
|
|
||||||
if (txSets[nextTx?.txid]?.modified) {
|
|
||||||
nextTx = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nextTx = modified.shift();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextTx && txSets[nextTx.txid]?.available) {
|
// Select best next package
|
||||||
const nextTxSet = txSets[nextTx.txid];
|
let nextTx;
|
||||||
|
const nextPoolTx = mempoolArray[top];
|
||||||
|
const nextModifiedTx = modified.peek();
|
||||||
|
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
|
||||||
|
nextTx = nextPoolTx;
|
||||||
|
top++;
|
||||||
|
} else {
|
||||||
|
modified.pop();
|
||||||
|
if (nextModifiedTx) {
|
||||||
|
nextTx = nextModifiedTx;
|
||||||
|
nextTx.modifiedNode = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextTx && !nextTx?.used) {
|
||||||
// Check if the package fits into this block
|
// Check if the package fits into this block
|
||||||
if (nextTxSet && blockWeight + nextTxSet.weight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
||||||
blockWeight += nextTxSet.weight;
|
blockWeight += nextTx.ancestorWeight;
|
||||||
// sort txSet by dependency graph (equivalent to sorting by ascending ancestor count)
|
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||||
const sortedTxSet = [...nextTx.ancestors.sort((a, b) => {
|
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||||
return (mempool[a.txid]?.ancestors?.length || 0) - (mempool[b.txid]?.ancestors?.length || 0);
|
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||||
}), nextTx];
|
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
|
||||||
sortedTxSet.forEach((ancestor, i, arr) => {
|
sortedTxSet.forEach((ancestor, i, arr) => {
|
||||||
const tx = mempool[ancestor.txid];
|
const mempoolTx = mempool[ancestor.txid];
|
||||||
const txSet = txSets[ancestor.txid];
|
if (ancestor && !ancestor?.used) {
|
||||||
if (txSet.available) {
|
ancestor.used = true;
|
||||||
txSet.available = false;
|
// update original copy of this tx with effective fee rate & relatives data
|
||||||
tx.effectiveFeePerVsize = nextTxSet.fee / (nextTxSet.weight / 4);
|
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
|
||||||
tx.cpfpChecked = true;
|
mempoolTx.ancestors = (Array.from(ancestor.ancestorMap?.values()) as AuditTransaction[]).map((a) => {
|
||||||
|
return {
|
||||||
|
txid: a.txid,
|
||||||
|
fee: a.fee,
|
||||||
|
weight: a.weight,
|
||||||
|
}
|
||||||
|
})
|
||||||
if (i < arr.length - 1) {
|
if (i < arr.length - 1) {
|
||||||
tx.bestDescendant = {
|
mempoolTx.bestDescendant = {
|
||||||
txid: arr[i + 1].txid,
|
txid: arr[arr.length - 1].txid,
|
||||||
fee: arr[i + 1].fee,
|
fee: arr[arr.length - 1].fee,
|
||||||
weight: arr[i + 1].weight,
|
weight: arr[arr.length - 1].weight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
transactions.push(tx);
|
transactions.push(ancestor);
|
||||||
blockSize += tx.size;
|
blockSize += ancestor.size;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove these as valid package ancestors for any remaining descendants
|
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||||
if (!simpleMode) {
|
if (sortedTxSet.length) {
|
||||||
sortedTxSet.forEach(tx => {
|
sortedTxSet.forEach(tx => {
|
||||||
anyModified = this.updateDescendants(tx, tx, mempool, txSets, modified);
|
this.updateDescendants(tx, auditPool, modified);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
failures = 0;
|
failures = 0;
|
||||||
} else {
|
} else {
|
||||||
// hold this package in an overflow list while we check for smaller options
|
// hold this package in an overflow list while we check for smaller options
|
||||||
txSets[nextTx.txid].modified = true;
|
|
||||||
overflow.push(nextTx);
|
overflow.push(nextTx);
|
||||||
failures++;
|
failures++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this block is full
|
// this block is full
|
||||||
const outOfTransactions = !mempoolArray.length && !modified.length;
|
|
||||||
const exceededPackageTries = failures > 1000 && blockWeight > (config.MEMPOOL.BLOCK_WEIGHT_UNITS - 4000);
|
const exceededPackageTries = failures > 1000 && blockWeight > (config.MEMPOOL.BLOCK_WEIGHT_UNITS - 4000);
|
||||||
const exceededSimpleTries = failures > 0 && simpleMode;
|
if (exceededPackageTries && (!condenseRest || blocks.length < blockLimit - 1)) {
|
||||||
if (outOfTransactions || exceededPackageTries || exceededSimpleTries) {
|
|
||||||
// construct this block
|
// construct this block
|
||||||
blocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, blocks.length));
|
if (transactions.length) {
|
||||||
|
blocks.push(this.dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length));
|
||||||
|
}
|
||||||
// reset for the next block
|
// reset for the next block
|
||||||
transactions = [];
|
transactions = [];
|
||||||
blockSize = 0;
|
blockSize = 0;
|
||||||
blockWeight = 4000;
|
blockWeight = 4000;
|
||||||
|
|
||||||
// 'overflow' packages didn't fit in this block, but are valid candidates for the next
|
// 'overflow' packages didn't fit in this block, but are valid candidates for the next
|
||||||
if (overflow.length) {
|
for (const overflowTx of overflow.reverse()) {
|
||||||
modified = modified.concat(overflow);
|
if (overflowTx.modified) {
|
||||||
overflow = [];
|
overflowTx.modifiedNode = modified.add(overflowTx);
|
||||||
anyModified = true;
|
} else {
|
||||||
|
top--;
|
||||||
|
mempoolArray[top] = overflowTx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
overflow = [];
|
||||||
|
|
||||||
// re-sort modified list if necessary
|
|
||||||
if (anyModified) {
|
|
||||||
modified = modified.filter(tx => txSets[tx.txid]?.available).sort(byAncestor);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (condenseRest) {
|
||||||
|
// pack any leftover transactions into the last block
|
||||||
|
for (const tx of overflow) {
|
||||||
|
if (!tx || tx?.used) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
blockWeight += tx.weight;
|
||||||
|
blockSize += tx.size;
|
||||||
|
transactions.push(tx);
|
||||||
|
tx.used = true;
|
||||||
|
}
|
||||||
|
const blockTransactions = transactions.map(t => mempool[t.txid])
|
||||||
|
restOfArray.forEach(tx => {
|
||||||
|
blockWeight += tx.weight;
|
||||||
|
blockSize += tx.size;
|
||||||
|
blockTransactions.push(tx);
|
||||||
|
});
|
||||||
|
if (blockTransactions.length) {
|
||||||
|
blocks.push(this.dataToMempoolBlocks(blockTransactions, blockSize, blockWeight, blocks.length));
|
||||||
|
}
|
||||||
|
transactions = [];
|
||||||
|
} else if (transactions.length) {
|
||||||
|
blocks.push(this.dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length));
|
||||||
|
}
|
||||||
|
|
||||||
const end = new Date().getTime();
|
const end = Date.now();
|
||||||
const time = end - start;
|
const time = end - start;
|
||||||
logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
|
logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
|
||||||
|
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortByAncestorScore(a, b, txSets): number {
|
// traverse in-mempool ancestors
|
||||||
return txSets[b.txid]?.score - txSets[a.txid]?.score;
|
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
||||||
|
public setRelatives(
|
||||||
|
tx: AuditTransaction,
|
||||||
|
mempool: { [txid: string]: AuditTransaction },
|
||||||
|
): void {
|
||||||
|
for (const parent of tx.vin) {
|
||||||
|
const parentTx = mempool[parent.txid];
|
||||||
|
if (parentTx && !tx.ancestorMap!.has(parent.txid)) {
|
||||||
|
tx.ancestorMap.set(parent.txid, parentTx);
|
||||||
|
parentTx.children.add(tx);
|
||||||
|
// visit each node only once
|
||||||
|
if (!parentTx.relativesSet) {
|
||||||
|
this.setRelatives(parentTx, mempool);
|
||||||
|
}
|
||||||
|
parentTx.ancestorMap.forEach((ancestor) => {
|
||||||
|
tx.ancestorMap.set(ancestor.txid, ancestor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tx.ancestorFee = tx.fee || 0;
|
||||||
|
tx.ancestorWeight = tx.weight || 0;
|
||||||
|
tx.ancestorMap.forEach((ancestor) => {
|
||||||
|
tx.ancestorFee += ancestor.fee;
|
||||||
|
tx.ancestorWeight += ancestor.weight;
|
||||||
|
});
|
||||||
|
tx.score = tx.ancestorFee / (tx.ancestorWeight || 1);
|
||||||
|
tx.relativesSet = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setRelatives(tx: TransactionExtended, mempool: { [txid: string]: TransactionExtended }, txSets: { [txid: string]: TransactionSet }): { [txid: string]: Ancestor } {
|
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
|
||||||
let ancestors: { [txid: string]: Ancestor } = {};
|
// avoids recursion to limit call stack depth
|
||||||
tx.vin.forEach((parent) => {
|
private updateDescendants(
|
||||||
const parentTx = mempool[parent.txid];
|
rootTx: AuditTransaction,
|
||||||
const parentTxSet = txSets[parent.txid];
|
mempool: { [txid: string]: AuditTransaction },
|
||||||
if (parentTx && parentTxSet) {
|
modified: PairingHeap<AuditTransaction>,
|
||||||
ancestors[parentTx.txid] = parentTx;
|
): void {
|
||||||
if (!parentTxSet.children) {
|
const descendantSet: Set<AuditTransaction> = new Set();
|
||||||
parentTxSet.children = [tx.txid];
|
// stack of nodes left to visit
|
||||||
} else {
|
const descendants: AuditTransaction[] = [];
|
||||||
parentTxSet.children.push(tx.txid);
|
let descendantTx;
|
||||||
}
|
let ancestorIndex;
|
||||||
if (!parentTxSet.score) {
|
let tmpScore;
|
||||||
ancestors = {
|
rootTx.children.forEach(childTx => {
|
||||||
...ancestors,
|
if (!descendantSet.has(childTx)) {
|
||||||
...this.setRelatives(parentTx, mempool, txSets),
|
descendants.push(childTx);
|
||||||
};
|
descendantSet.add(childTx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tx.ancestors = Object.values(ancestors).map(ancestor => {
|
while (descendants.length) {
|
||||||
return {
|
descendantTx = descendants.pop();
|
||||||
txid: ancestor.txid,
|
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
||||||
fee: ancestor.fee,
|
// remove tx as ancestor
|
||||||
weight: ancestor.weight
|
descendantTx.ancestorMap.delete(rootTx.txid);
|
||||||
};
|
descendantTx.ancestorFee -= rootTx.fee;
|
||||||
});
|
descendantTx.ancestorWeight -= rootTx.weight;
|
||||||
let totalFees = tx.fee;
|
tmpScore = descendantTx.score;
|
||||||
let totalWeight = tx.weight;
|
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorWeight;
|
||||||
tx.ancestors.forEach(ancestor => {
|
|
||||||
totalFees += ancestor.fee;
|
|
||||||
totalWeight += ancestor.weight;
|
|
||||||
});
|
|
||||||
txSets[tx.txid].fee = totalFees;
|
|
||||||
txSets[tx.txid].weight = totalWeight;
|
|
||||||
txSets[tx.txid].score = this.calcAncestorScore(tx, totalFees, totalWeight);
|
|
||||||
|
|
||||||
return ancestors;
|
if (!descendantTx.modifiedNode) {
|
||||||
}
|
descendantTx.modified = true;
|
||||||
|
descendantTx.modifiedNode = modified.add(descendantTx);
|
||||||
private calcAncestorScore(tx: TransactionExtended, ancestorFees: number, ancestorWeight: number): number {
|
} else {
|
||||||
return Math.min(tx.fee / tx.weight, ancestorFees / ancestorWeight);
|
// rebalance modified heap if score has changed
|
||||||
}
|
if (descendantTx.score < tmpScore) {
|
||||||
|
modified.decreasePriority(descendantTx.modifiedNode);
|
||||||
// walk over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
|
} else if (descendantTx.score > tmpScore) {
|
||||||
// returns whether any descendants were modified
|
modified.increasePriority(descendantTx.modifiedNode);
|
||||||
private updateDescendants(
|
|
||||||
root: TransactionExtended,
|
|
||||||
tx: TransactionExtended,
|
|
||||||
mempool: { [txid: string]: TransactionExtended },
|
|
||||||
txSets: { [txid: string]: TransactionSet },
|
|
||||||
modified: TransactionExtended[],
|
|
||||||
): boolean {
|
|
||||||
let anyModified = false;
|
|
||||||
const txSet = txSets[tx.txid];
|
|
||||||
if (txSet.children) {
|
|
||||||
txSet.children.forEach(childId => {
|
|
||||||
const child = mempool[childId];
|
|
||||||
if (child && child.ancestors) {
|
|
||||||
const ancestorIndex = child.ancestors.findIndex(a => a.txid === root.txid);
|
|
||||||
if (ancestorIndex > -1) {
|
|
||||||
// remove tx as ancestor
|
|
||||||
child.ancestors.splice(ancestorIndex, 1);
|
|
||||||
const childTxSet = txSets[childId];
|
|
||||||
childTxSet.fee -= root.fee;
|
|
||||||
childTxSet.weight -= root.weight;
|
|
||||||
childTxSet.score = this.calcAncestorScore(child, childTxSet.fee, childTxSet.weight);
|
|
||||||
anyModified = true;
|
|
||||||
|
|
||||||
if (!childTxSet.modified) {
|
|
||||||
childTxSet.modified = true;
|
|
||||||
modified.push(child);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// recursively update grandchildren
|
|
||||||
if (child) {
|
// add this node's children to the stack
|
||||||
anyModified = this.updateDescendants(root, child, mempool, txSets, modified) || anyModified;
|
descendantTx.children.forEach(childTx => {
|
||||||
}
|
// visit each node only once
|
||||||
});
|
if (!descendantSet.has(childTx)) {
|
||||||
|
descendants.push(childTx);
|
||||||
|
descendantSet.add(childTx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return anyModified;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
||||||
|
@ -250,6 +250,8 @@ class WebsocketHandler {
|
|||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug("mempool changed!");
|
||||||
|
|
||||||
mempoolBlocks.updateMempoolBlocks(newMempool);
|
mempoolBlocks.updateMempoolBlocks(newMempool);
|
||||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
@ -417,7 +419,7 @@ class WebsocketHandler {
|
|||||||
const _memPool = memPool.getMempool();
|
const _memPool = memPool.getMempool();
|
||||||
const mempoolCopy = cloneMempool(_memPool);
|
const mempoolCopy = cloneMempool(_memPool);
|
||||||
|
|
||||||
const projectedBlocks = mempoolBlocks.makeBlockTemplates(mempoolCopy, 2, 2);
|
const projectedBlocks = mempoolBlocks.makeBlockTemplates(mempoolCopy, 2);
|
||||||
|
|
||||||
if (projectedBlocks[0]) {
|
if (projectedBlocks[0]) {
|
||||||
const { censored, added, score } = Audit.auditBlock(block, txIds, transactions, projectedBlocks, mempoolCopy);
|
const { censored, added, score } = Audit.auditBlock(block, txIds, transactions, projectedBlocks, mempoolCopy);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
|
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
|
||||||
|
import { HeapNode } from "./utils/pairing-heap";
|
||||||
|
|
||||||
export interface PoolTag {
|
export interface PoolTag {
|
||||||
id: number; // mysql row id
|
id: number; // mysql row id
|
||||||
@ -70,6 +71,24 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
|||||||
deleteAfter?: number;
|
deleteAfter?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuditTransaction {
|
||||||
|
txid: string;
|
||||||
|
fee: number;
|
||||||
|
size: number;
|
||||||
|
weight: number;
|
||||||
|
feePerVsize: number;
|
||||||
|
vin: IEsploraApi.Vin[];
|
||||||
|
relativesSet: boolean;
|
||||||
|
ancestorMap: Map<string, AuditTransaction>;
|
||||||
|
children: Set<AuditTransaction>;
|
||||||
|
ancestorFee: number;
|
||||||
|
ancestorWeight: number;
|
||||||
|
score: number;
|
||||||
|
used: boolean;
|
||||||
|
modified: boolean;
|
||||||
|
modifiedNode: HeapNode<AuditTransaction>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Ancestor {
|
export interface Ancestor {
|
||||||
txid: string;
|
txid: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
@ -80,9 +99,10 @@ export interface TransactionSet {
|
|||||||
fee: number;
|
fee: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
score: number;
|
score: number;
|
||||||
children?: string[];
|
children?: Set<string>;
|
||||||
available?: boolean;
|
available?: boolean;
|
||||||
modified?: boolean;
|
modified?: boolean;
|
||||||
|
modifiedNode?: HeapNode<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BestDescendant {
|
interface BestDescendant {
|
||||||
|
174
backend/src/utils/pairing-heap.ts
Normal file
174
backend/src/utils/pairing-heap.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
export type HeapNode<T> = {
|
||||||
|
element: T
|
||||||
|
child?: HeapNode<T>
|
||||||
|
next?: HeapNode<T>
|
||||||
|
prev?: HeapNode<T>
|
||||||
|
} | null | undefined;
|
||||||
|
|
||||||
|
// minimal pairing heap priority queue implementation
|
||||||
|
export class PairingHeap<T> {
|
||||||
|
private root: HeapNode<T> = null;
|
||||||
|
private comparator: (a: T, b: T) => boolean;
|
||||||
|
|
||||||
|
// comparator function should return 'true' if a is higher priority than b
|
||||||
|
constructor(comparator: (a: T, b: T) => boolean) {
|
||||||
|
this.comparator = comparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty(): boolean {
|
||||||
|
return !this.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(element: T): HeapNode<T> {
|
||||||
|
const node: HeapNode<T> = {
|
||||||
|
element
|
||||||
|
};
|
||||||
|
|
||||||
|
this.root = this.meld(this.root, node);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the top priority element without modifying the queue
|
||||||
|
peek(): T | void {
|
||||||
|
return this.root?.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes and returns the top priority element
|
||||||
|
pop(): T | void {
|
||||||
|
let element;
|
||||||
|
if (this.root) {
|
||||||
|
const node = this.root;
|
||||||
|
element = node.element;
|
||||||
|
this.root = this.mergePairs(node.child);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNode(node: HeapNode<T>): void {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === this.root) {
|
||||||
|
this.root = this.mergePairs(node.child);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (node.prev) {
|
||||||
|
if (node.prev.child === node) {
|
||||||
|
node.prev.child = node.next;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node.prev.next = node.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.next) {
|
||||||
|
node.next.prev = node.prev;
|
||||||
|
}
|
||||||
|
this.root = this.meld(this.root, this.mergePairs(node.child));
|
||||||
|
}
|
||||||
|
|
||||||
|
node.child = null;
|
||||||
|
node.prev = null;
|
||||||
|
node.next = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix the heap after increasing the priority of a given node
|
||||||
|
increasePriority(node: HeapNode<T>): void {
|
||||||
|
// already the top priority element
|
||||||
|
if (!node || node === this.root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// extract from siblings
|
||||||
|
if (node.prev) {
|
||||||
|
if (node.prev?.child === node) {
|
||||||
|
if (this.comparator(node.prev.element, node.element)) {
|
||||||
|
// already in a valid position
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.prev.child = node.next;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node.prev.next = node.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.next) {
|
||||||
|
node.next.prev = node.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root = this.meld(this.root, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
decreasePriority(node: HeapNode<T>): void {
|
||||||
|
this.deleteNode(node);
|
||||||
|
this.root = this.meld(this.root, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
meld(a: HeapNode<T>, b: HeapNode<T>): HeapNode<T> {
|
||||||
|
if (!a) {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
if (!b || a === b) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent: HeapNode<T> = b;
|
||||||
|
let child: HeapNode<T> = a;
|
||||||
|
if (this.comparator(a.element, b.element)) {
|
||||||
|
parent = a;
|
||||||
|
child = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.next = parent.child;
|
||||||
|
if (parent.child) {
|
||||||
|
parent.child.prev = child;
|
||||||
|
}
|
||||||
|
child.prev = parent;
|
||||||
|
parent.child = child;
|
||||||
|
|
||||||
|
parent.next = null;
|
||||||
|
parent.prev = null;
|
||||||
|
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergePairs(node: HeapNode<T>): HeapNode<T> {
|
||||||
|
if (!node) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current: HeapNode<T> = node;
|
||||||
|
let next: HeapNode<T>;
|
||||||
|
let nextCurrent: HeapNode<T>;
|
||||||
|
let pairs: HeapNode<T>;
|
||||||
|
let melded: HeapNode<T>;
|
||||||
|
while (current) {
|
||||||
|
next = current.next;
|
||||||
|
if (next) {
|
||||||
|
nextCurrent = next.next;
|
||||||
|
melded = this.meld(current, next);
|
||||||
|
if (melded) {
|
||||||
|
melded.prev = pairs;
|
||||||
|
}
|
||||||
|
pairs = melded;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nextCurrent = null;
|
||||||
|
current.prev = pairs;
|
||||||
|
pairs = current;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = nextCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
melded = null;
|
||||||
|
let prev: HeapNode<T>;
|
||||||
|
while (pairs) {
|
||||||
|
prev = pairs.prev;
|
||||||
|
melded = this.meld(melded, pairs);
|
||||||
|
pairs = prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return melded;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user