Optimize makeBlockTemplates

This commit is contained in:
Mononaut 2022-10-27 10:21:39 -06:00
parent 832ccdac46
commit 968d7b827b
No known key found for this signature in database
GPG Key ID: 61B952CAF4838F94
5 changed files with 406 additions and 160 deletions

View File

@ -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

View File

@ -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[],

View File

@ -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);

View File

@ -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 {

View 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;
}
}