mirror of
https://github.com/mempool/mempool.git
synced 2025-03-30 04:32:06 +02:00
426 lines
15 KiB
TypeScript
426 lines
15 KiB
TypeScript
import { TransactionExtended, MempoolTransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
|
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
|
import { Common } from './common';
|
|
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
|
import logger from '../logger';
|
|
import config from '../config';
|
|
import pLimit from '../utils/p-limit';
|
|
|
|
class TransactionUtils {
|
|
constructor() { }
|
|
|
|
public stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
|
|
return {
|
|
vin: [{
|
|
scriptsig: tx.vin[0].scriptsig || tx.vin[0]['coinbase']
|
|
}],
|
|
vout: tx.vout
|
|
.map((vout) => ({
|
|
scriptpubkey_address: vout.scriptpubkey_address,
|
|
scriptpubkey_asm: vout.scriptpubkey_asm,
|
|
value: vout.value
|
|
}))
|
|
.filter((vout) => vout.value)
|
|
};
|
|
}
|
|
|
|
// Wrapper for $getTransactionExtended with an automatic retry direct to Core if the first API request fails.
|
|
// Propagates any error from the retry request.
|
|
public async $getTransactionExtendedRetry(txid: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
|
try {
|
|
const result = await this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, addMempoolData);
|
|
if (result) {
|
|
return result;
|
|
} else {
|
|
logger.err(`Cannot fetch tx ${txid}. Reason: backend returned null data`);
|
|
}
|
|
} catch (e) {
|
|
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
|
|
}
|
|
// retry direct from Core if first request failed
|
|
return this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, true, addMempoolData);
|
|
}
|
|
|
|
/**
|
|
* @param txId
|
|
* @param addPrevouts
|
|
* @param lazyPrevouts
|
|
* @param forceCore - See https://github.com/mempool/mempool/issues/2904
|
|
*/
|
|
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
|
let transaction: IEsploraApi.Transaction;
|
|
if (forceCore === true) {
|
|
transaction = await bitcoinCoreApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
|
} else {
|
|
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
|
}
|
|
|
|
if (Common.isLiquid()) {
|
|
if (!isFinite(Number(transaction.fee))) {
|
|
transaction.fee = Object.values(transaction.fee || {}).reduce((total, output) => total + output, 0);
|
|
}
|
|
}
|
|
|
|
if (addMempoolData || !transaction?.status?.confirmed) {
|
|
return this.extendMempoolTransaction(transaction);
|
|
} else {
|
|
return this.extendTransaction(transaction);
|
|
}
|
|
}
|
|
|
|
public async $getMempoolTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended> {
|
|
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
|
|
}
|
|
|
|
public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended[]> {
|
|
if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') {
|
|
const limiter = pLimit(8); // Run 8 requests at a time
|
|
const results = await Promise.allSettled(txids.map(
|
|
txid => limiter(() => this.$getMempoolTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore))
|
|
));
|
|
return results.filter(reply => reply.status === 'fulfilled')
|
|
.map(r => (r as PromiseFulfilledResult<MempoolTransactionExtended>).value);
|
|
} else {
|
|
const transactions = await bitcoinApi.$getMempoolTransactions(txids);
|
|
return transactions.map(transaction => {
|
|
if (Common.isLiquid()) {
|
|
if (!isFinite(Number(transaction.fee))) {
|
|
transaction.fee = Object.values(transaction.fee || {}).reduce((total, output) => total + output, 0);
|
|
}
|
|
}
|
|
|
|
return this.extendMempoolTransaction(transaction);
|
|
});
|
|
}
|
|
}
|
|
|
|
public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
|
|
// @ts-ignore
|
|
if (transaction.vsize) {
|
|
// @ts-ignore
|
|
return transaction;
|
|
}
|
|
const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4);
|
|
const transactionExtended: TransactionExtended = Object.assign({
|
|
vsize: transaction.weight / 4,
|
|
feePerVsize: feePerVbytes,
|
|
effectiveFeePerVsize: feePerVbytes,
|
|
}, transaction);
|
|
if (!transaction?.status?.confirmed && !transactionExtended.firstSeen) {
|
|
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
|
}
|
|
return transactionExtended;
|
|
}
|
|
|
|
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
|
|
const vsize = Math.ceil(transaction.weight / 4);
|
|
const fractionalVsize = (transaction.weight / 4);
|
|
let sigops = Common.isLiquid() ? 0 : (transaction.sigops != null ? transaction.sigops : this.countSigops(transaction));
|
|
// https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298
|
|
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
|
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
|
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
|
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
|
|
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
|
order: this.txidToOrdering(transaction.txid),
|
|
vsize,
|
|
adjustedVsize,
|
|
sigops,
|
|
feePerVsize: feePerVbytes,
|
|
adjustedFeePerVsize: adjustedFeePerVsize,
|
|
effectiveFeePerVsize: effectiveFeePerVsize,
|
|
});
|
|
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
|
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
|
}
|
|
return transactionExtended;
|
|
}
|
|
|
|
public hex2ascii(hex: string) {
|
|
let str = '';
|
|
for (let i = 0; i < hex.length; i += 2) {
|
|
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
|
|
}
|
|
return str;
|
|
}
|
|
|
|
public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
|
|
if (!script?.length) {
|
|
return 0;
|
|
}
|
|
|
|
let sigops = 0;
|
|
// count OP_CHECKSIG and OP_CHECKSIGVERIFY
|
|
sigops += (script.match(/OP_CHECKSIG/g)?.length || 0);
|
|
|
|
// count OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY
|
|
if (isRawScript) {
|
|
// in scriptPubKey or scriptSig, always worth 20
|
|
sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0);
|
|
} else {
|
|
// in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
|
|
const matches = script.matchAll(/(?:OP_(?:PUSHNUM_)?(\d+))? OP_CHECKMULTISIG/g);
|
|
for (const match of matches) {
|
|
const n = parseInt(match[1]);
|
|
if (Number.isInteger(n)) {
|
|
sigops += n;
|
|
} else {
|
|
sigops += 20;
|
|
}
|
|
}
|
|
}
|
|
|
|
return witness ? sigops : (sigops * 4);
|
|
}
|
|
|
|
public countSigops(transaction: IEsploraApi.Transaction): number {
|
|
let sigops = 0;
|
|
|
|
for (const input of transaction.vin) {
|
|
if (input.scriptsig_asm) {
|
|
sigops += this.countScriptSigops(input.scriptsig_asm, true);
|
|
}
|
|
if (input.prevout) {
|
|
switch (true) {
|
|
case input.prevout.scriptpubkey_type === 'p2sh' && input.witness?.length === 2 && input.scriptsig && input.scriptsig.startsWith('160014'):
|
|
case input.prevout.scriptpubkey_type === 'v0_p2wpkh':
|
|
sigops += 1;
|
|
break;
|
|
|
|
case input.prevout?.scriptpubkey_type === 'p2sh' && input.witness?.length && input.scriptsig && input.scriptsig.startsWith('220020'):
|
|
case input.prevout.scriptpubkey_type === 'v0_p2wsh':
|
|
if (input.witness?.length) {
|
|
sigops += this.countScriptSigops(bitcoinjs.script.toASM(Buffer.from(input.witness[input.witness.length - 1], 'hex')), false, true);
|
|
}
|
|
break;
|
|
|
|
case input.prevout.scriptpubkey_type === 'p2sh':
|
|
if (input.inner_redeemscript_asm) {
|
|
sigops += this.countScriptSigops(input.inner_redeemscript_asm);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const output of transaction.vout) {
|
|
if (output.scriptpubkey_asm) {
|
|
sigops += this.countScriptSigops(output.scriptpubkey_asm, true);
|
|
}
|
|
}
|
|
|
|
return sigops;
|
|
}
|
|
|
|
// returns the most significant 4 bytes of the txid as an integer
|
|
public txidToOrdering(txid: string): number {
|
|
return parseInt(
|
|
txid.substr(62, 2) +
|
|
txid.substr(60, 2) +
|
|
txid.substr(58, 2) +
|
|
txid.substr(56, 2),
|
|
16
|
|
);
|
|
}
|
|
|
|
public addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
|
|
if (!vin.prevout) {
|
|
return;
|
|
}
|
|
|
|
if (vin.prevout.scriptpubkey_type === 'p2sh') {
|
|
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
|
|
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
|
|
if (vin.witness && vin.witness.length > 2) {
|
|
const witnessScript = vin.witness[vin.witness.length - 1];
|
|
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
|
}
|
|
}
|
|
|
|
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
|
|
const witnessScript = vin.witness[vin.witness.length - 1];
|
|
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
|
}
|
|
|
|
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
|
|
const witnessScript = this.witnessToP2TRScript(vin.witness);
|
|
if (witnessScript !== null) {
|
|
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
|
}
|
|
}
|
|
}
|
|
|
|
public convertScriptSigAsm(hex: string): string {
|
|
const buf = Buffer.from(hex, 'hex');
|
|
|
|
const b: string[] = [];
|
|
|
|
let i = 0;
|
|
while (i < buf.length) {
|
|
const op = buf[i];
|
|
if (op >= 0x01 && op <= 0x4e) {
|
|
i++;
|
|
let push: number;
|
|
if (op === 0x4c) {
|
|
push = buf.readUInt8(i);
|
|
b.push('OP_PUSHDATA1');
|
|
i += 1;
|
|
} else if (op === 0x4d) {
|
|
push = buf.readUInt16LE(i);
|
|
b.push('OP_PUSHDATA2');
|
|
i += 2;
|
|
} else if (op === 0x4e) {
|
|
push = buf.readUInt32LE(i);
|
|
b.push('OP_PUSHDATA4');
|
|
i += 4;
|
|
} else {
|
|
push = op;
|
|
b.push('OP_PUSHBYTES_' + push);
|
|
}
|
|
|
|
const data = buf.slice(i, i + push);
|
|
if (data.length !== push) {
|
|
break;
|
|
}
|
|
|
|
b.push(data.toString('hex'));
|
|
i += data.length;
|
|
} else {
|
|
if (op === 0x00) {
|
|
b.push('OP_0');
|
|
} else if (op === 0x4f) {
|
|
b.push('OP_PUSHNUM_NEG1');
|
|
} else if (op === 0xb1) {
|
|
b.push('OP_CLTV');
|
|
} else if (op === 0xb2) {
|
|
b.push('OP_CSV');
|
|
} else if (op === 0xba) {
|
|
b.push('OP_CHECKSIGADD');
|
|
} else {
|
|
const opcode = bitcoinjs.script.toASM([ op ]);
|
|
if (opcode && op < 0xfd) {
|
|
if (/^OP_(\d+)$/.test(opcode)) {
|
|
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
|
|
} else {
|
|
b.push(opcode);
|
|
}
|
|
} else {
|
|
b.push('OP_RETURN_' + op);
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
return b.join(' ');
|
|
}
|
|
|
|
/**
|
|
* This function must only be called when we know the witness we are parsing
|
|
* is a taproot witness.
|
|
* @param witness An array of hex strings that represents the witness stack of
|
|
* the input.
|
|
* @returns null if the witness is not a script spend, and the hex string of
|
|
* the script item if it is a script spend.
|
|
*/
|
|
public witnessToP2TRScript(witness: string[]): string | null {
|
|
if (witness.length < 2) return null;
|
|
// Note: see BIP341 for parsing details of witness stack
|
|
|
|
// If there are at least two witness elements, and the first byte of the
|
|
// last element is 0x50, this last element is called annex a and
|
|
// is removed from the witness stack.
|
|
const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
|
|
// If there are at least two witness elements left, script path spending is used.
|
|
// Call the second-to-last stack element s, the script.
|
|
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
|
|
if (hasAnnex && witness.length < 3) return null;
|
|
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
|
return witness[positionOfScript];
|
|
}
|
|
|
|
// calculate the most parsimonious set of prioritizations given a list of block transactions
|
|
// (i.e. the most likely prioritizations and deprioritizations)
|
|
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
|
|
// find the longest increasing subsequence of transactions
|
|
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
|
|
// should be O(n log n)
|
|
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
|
|
if (X.length < 2) {
|
|
return { prioritized: [], deprioritized: [] };
|
|
}
|
|
const N = X.length;
|
|
const P: number[] = new Array(N);
|
|
const M: number[] = new Array(N + 1);
|
|
M[0] = -1; // undefined so can be set to any value
|
|
|
|
let L = 0;
|
|
for (let i = 0; i < N; i++) {
|
|
// Binary search for the smallest positive l ≤ L
|
|
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
|
|
let lo = 1;
|
|
let hi = L + 1;
|
|
while (lo < hi) {
|
|
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
|
|
if (X[M[mid]].rate > X[i].rate) {
|
|
hi = mid;
|
|
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
|
|
lo = mid + 1;
|
|
}
|
|
}
|
|
|
|
// After searching, lo == hi is 1 greater than the
|
|
// length of the longest prefix of X[i]
|
|
const newL = lo;
|
|
|
|
// The predecessor of X[i] is the last index of
|
|
// the subsequence of length newL-1
|
|
P[i] = M[newL - 1];
|
|
M[newL] = i;
|
|
|
|
if (newL > L) {
|
|
// If we found a subsequence longer than any we've
|
|
// found yet, update L
|
|
L = newL;
|
|
}
|
|
}
|
|
|
|
// Reconstruct the longest increasing subsequence
|
|
// It consists of the values of X at the L indices:
|
|
// ..., P[P[M[L]]], P[M[L]], M[L]
|
|
const LIS: any[] = new Array(L);
|
|
let k = M[L];
|
|
for (let j = L - 1; j >= 0; j--) {
|
|
LIS[j] = X[k];
|
|
k = P[k];
|
|
}
|
|
|
|
const lisMap = new Map<string, number>();
|
|
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
|
|
|
|
const prioritized: string[] = [];
|
|
const deprioritized: string[] = [];
|
|
|
|
let lastRate = X[0].rate;
|
|
|
|
for (const tx of X) {
|
|
if (lisMap.has(tx.txid)) {
|
|
lastRate = tx.rate;
|
|
} else {
|
|
if (Math.abs(tx.rate - lastRate) < 0.1) {
|
|
// skip if the rate is almost the same as the previous transaction
|
|
} else if (tx.rate <= lastRate) {
|
|
prioritized.push(tx.txid);
|
|
} else {
|
|
deprioritized.push(tx.txid);
|
|
}
|
|
}
|
|
}
|
|
|
|
return { prioritized, deprioritized };
|
|
}
|
|
}
|
|
|
|
export default new TransactionUtils();
|