2023-04-19 18:10:10 -07:00
import * as bitcoinjs from 'bitcoinjs-lib' ;
import { Request } from 'express' ;
2023-12-21 16:20:31 +00:00
import { CpfpInfo , CpfpSummary , CpfpCluster , EffectiveFeeStats , MempoolBlockWithTransactions , TransactionExtended , MempoolTransactionExtended , TransactionStripped , WorkingEffectiveFeeStats , TransactionClassified , TransactionFlags } from '../mempool.interfaces' ;
2021-08-05 02:03:52 +03:00
import config from '../config' ;
2022-08-13 10:24:11 +02:00
import { NodeSocket } from '../repositories/NodesSocketsRepository' ;
import { isIP } from 'net' ;
2023-12-05 06:54:31 +00:00
import transactionUtils from './transaction-utils' ;
2023-12-14 11:26:17 +00:00
import { isPoint } from '../utils/secp256k1' ;
2024-01-30 16:41:35 +00:00
import logger from '../logger' ;
2024-02-24 22:47:50 +00:00
import { getVarIntLength , opcodes , parseMultisigScript } from '../utils/bitcoin-script' ;
// Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2 ;
const MAX_STANDARD_TX_WEIGHT = 400 _000 ;
const MAX_BLOCK_SIGOPS_COST = 80 _000 ;
const MAX_STANDARD_TX_SIGOPS_COST = ( MAX_BLOCK_SIGOPS_COST / 5 ) ;
const MIN_STANDARD_TX_NONWITNESS_SIZE = 65 ;
const MAX_P2SH_SIGOPS = 15 ;
const MAX_STANDARD_P2WSH_STACK_ITEMS = 100 ;
const MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80 ;
const MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE = 80 ;
const MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600 ;
const MAX_STANDARD_SCRIPTSIG_SIZE = 1650 ;
const DUST_RELAY_TX_FEE = 3 ;
const MAX_OP_RETURN_RELAY = 83 ;
const DEFAULT_PERMIT_BAREMULTISIG = true ;
2020-05-24 16:29:30 +07:00
export class Common {
2021-12-28 17:59:11 +04:00
static nativeAssetId = config . MEMPOOL . NETWORK === 'liquidtestnet' ?
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
: '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d' ;
2021-12-31 02:28:40 +04:00
static _isLiquid = config . MEMPOOL . NETWORK === 'liquid' || config . MEMPOOL . NETWORK === 'liquidtestnet' ;
static isLiquid ( ) : boolean {
return this . _isLiquid ;
}
2021-09-18 13:37:25 +04:00
2020-05-24 16:29:30 +07:00
static median ( numbers : number [ ] ) {
let medianNr = 0 ;
const numsLen = numbers . length ;
if ( numsLen % 2 === 0 ) {
medianNr = ( numbers [ numsLen / 2 - 1 ] + numbers [ numsLen / 2 ] ) / 2 ;
} else {
medianNr = numbers [ ( numsLen - 1 ) / 2 ] ;
}
return medianNr ;
}
2021-03-18 05:52:46 +00:00
static percentile ( numbers : number [ ] , percentile : number ) {
2021-04-10 21:26:05 +04:00
if ( percentile === 50 ) {
return this . median ( numbers ) ;
}
2021-03-18 05:52:46 +00:00
const index = Math . ceil ( numbers . length * ( 100 - percentile ) * 1 e - 2 ) ;
2021-04-10 21:26:05 +04:00
if ( index < 0 || index > numbers . length - 1 ) {
return 0 ;
}
2021-03-18 05:52:46 +00:00
return numbers [ index ] ;
}
2020-07-24 00:20:59 +07:00
static getFeesInRange ( transactions : TransactionExtended [ ] , rangeLength : number ) {
2023-01-19 11:09:03 -06:00
const filtered : TransactionExtended [ ] = [ ] ;
let lastValidRate = Infinity ;
// filter out anomalous fee rates to ensure monotonic range
for ( const tx of transactions ) {
if ( tx . effectiveFeePerVsize <= lastValidRate ) {
filtered . push ( tx ) ;
lastValidRate = tx . effectiveFeePerVsize ;
}
}
const arr = [ filtered [ filtered . length - 1 ] . effectiveFeePerVsize ] ;
2020-05-24 16:29:30 +07:00
const chunk = 1 / ( rangeLength - 1 ) ;
let itemsToAdd = rangeLength - 2 ;
while ( itemsToAdd > 0 ) {
2023-01-19 11:09:03 -06:00
arr . push ( filtered [ Math . floor ( filtered . length * chunk * itemsToAdd ) ] . effectiveFeePerVsize ) ;
2020-05-24 16:29:30 +07:00
itemsToAdd -- ;
}
2023-01-19 11:09:03 -06:00
arr . push ( filtered [ 0 ] . effectiveFeePerVsize ) ;
2020-05-24 16:29:30 +07:00
return arr ;
}
2020-06-08 18:55:53 +07:00
2023-08-05 17:20:07 +09:00
static findRbfTransactions ( added : MempoolTransactionExtended [ ] , deleted : MempoolTransactionExtended [ ] , forceScalable = false ) : { [ txid : string ] : MempoolTransactionExtended [ ] } {
2023-05-29 15:56:29 -04:00
const matches : { [ txid : string ] : MempoolTransactionExtended [ ] } = { } ;
2023-08-04 19:11:49 +09:00
// For small N, a naive nested loop is extremely fast, but it doesn't scale
2023-08-05 17:20:07 +09:00
if ( added . length < 1000 && deleted . length < 50 && ! forceScalable ) {
2023-08-04 19:11:49 +09:00
added . forEach ( ( addedTx ) = > {
2022-12-17 09:39:06 -06:00
const foundMatches = deleted . filter ( ( deletedTx ) = > {
2020-06-08 18:55:53 +07:00
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
return addedTx . fee > deletedTx . fee
// The new transaction must pay more fee per kB than the replaced tx.
2023-05-29 15:56:29 -04:00
&& addedTx . adjustedFeePerVsize > deletedTx . adjustedFeePerVsize
2020-06-08 18:55:53 +07:00
// Spends one or more of the same inputs
&& deletedTx . vin . some ( ( deletedVin ) = >
2022-12-14 08:56:46 -06:00
addedTx . vin . some ( ( vin ) = > vin . txid === deletedVin . txid && vin . vout === deletedVin . vout ) ) ;
2020-06-08 18:55:53 +07:00
} ) ;
2022-12-17 09:39:06 -06:00
if ( foundMatches ? . length ) {
2023-08-04 19:11:49 +09:00
matches [ addedTx . txid ] = [ . . . new Set ( foundMatches ) ] ;
2020-06-08 18:55:53 +07:00
}
} ) ;
2023-08-04 19:11:49 +09:00
} else {
// for large N, build a lookup table of prevouts we can check in ~constant time
const deletedSpendMap : { [ txid : string ] : { [ vout : number ] : MempoolTransactionExtended } } = { } ;
for ( const tx of deleted ) {
for ( const vin of tx . vin ) {
if ( ! deletedSpendMap [ vin . txid ] ) {
deletedSpendMap [ vin . txid ] = { } ;
}
deletedSpendMap [ vin . txid ] [ vin . vout ] = tx ;
}
}
for ( const addedTx of added ) {
const foundMatches = new Set < MempoolTransactionExtended > ( ) ;
for ( const vin of addedTx . vin ) {
const deletedTx = deletedSpendMap [ vin . txid ] ? . [ vin . vout ] ;
if ( deletedTx && deletedTx . txid !== addedTx . txid
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
&& addedTx . fee > deletedTx . fee
// The new transaction must pay more fee per kB than the replaced tx.
&& addedTx . adjustedFeePerVsize > deletedTx . adjustedFeePerVsize
) {
foundMatches . add ( deletedTx ) ;
}
if ( foundMatches . size ) {
matches [ addedTx . txid ] = [ . . . foundMatches ] ;
}
}
}
}
2020-06-08 18:55:53 +07:00
return matches ;
}
2020-09-26 02:11:30 +07:00
2023-05-31 11:37:13 -04:00
static findMinedRbfTransactions ( minedTransactions : TransactionExtended [ ] , spendMap : Map < string , MempoolTransactionExtended > ) : { [ txid : string ] : { replaced : MempoolTransactionExtended [ ] , replacedBy : TransactionExtended } } {
const matches : { [ txid : string ] : { replaced : MempoolTransactionExtended [ ] , replacedBy : TransactionExtended } } = { } ;
2023-05-18 09:51:41 -04:00
for ( const tx of minedTransactions ) {
2023-05-31 11:37:13 -04:00
const replaced : Set < MempoolTransactionExtended > = new Set ( ) ;
2023-05-18 09:51:41 -04:00
for ( let i = 0 ; i < tx . vin . length ; i ++ ) {
const vin = tx . vin [ i ] ;
const match = spendMap . get ( ` ${ vin . txid } : ${ vin . vout } ` ) ;
if ( match && match . txid !== tx . txid ) {
replaced . add ( match ) ;
2023-07-11 11:44:30 +09:00
// remove this tx from the spendMap
// prevents the same tx being replaced more than once
for ( const replacedVin of match . vin ) {
const key = ` ${ replacedVin . txid } : ${ replacedVin . vout } ` ;
spendMap . delete ( key ) ;
}
2023-05-18 09:51:41 -04:00
}
2023-07-11 11:44:30 +09:00
const key = ` ${ vin . txid } : ${ vin . vout } ` ;
spendMap . delete ( key ) ;
2023-05-18 09:51:41 -04:00
}
if ( replaced . size ) {
matches [ tx . txid ] = { replaced : Array.from ( replaced ) , replacedBy : tx } ;
}
}
return matches ;
}
2023-12-13 10:56:33 +00:00
static setSchnorrSighashFlags ( flags : bigint , witness : string [ ] ) : bigint {
// no witness items
if ( ! witness ? . length ) {
return flags ;
}
const hasAnnex = witness . length > 1 && witness [ witness . length - 1 ] . startsWith ( '50' ) ;
if ( witness ? . length === ( hasAnnex ? 2 : 1 ) ) {
// keypath spend, signature is the only witness item
if ( witness [ 0 ] . length === 130 ) {
flags |= this . setSighashFlags ( flags , witness [ 0 ] ) ;
} else {
flags |= TransactionFlags . sighash_default ;
}
} else {
// scriptpath spend, all items except for the script, control block and annex could be signatures
for ( let i = 0 ; i < witness . length - ( hasAnnex ? 3 : 2 ) ; i ++ ) {
// handle probable signatures
if ( witness [ i ] . length === 130 ) {
flags |= this . setSighashFlags ( flags , witness [ i ] ) ;
} else if ( witness [ i ] . length === 128 ) {
flags |= TransactionFlags . sighash_default ;
}
}
}
return flags ;
}
static isDERSig ( w : string ) : boolean {
// heuristic to detect probable DER signatures
return ( w . length >= 18
&& w . startsWith ( '30' ) // minimum DER signature length is 8 bytes + sighash flag (see https://mempool.space/testnet/tx/c6c232a36395fa338da458b86ff1327395a9afc28c5d2daa4273e410089fd433)
2023-12-13 16:15:55 +00:00
&& [ '01' , '02' , '03' , '81' , '82' , '83' ] . includes ( w . slice ( - 2 ) ) // signature must end with a valid sighash flag
&& ( w . length === ( 2 * parseInt ( w . slice ( 2 , 4 ) , 16 ) ) + 6 ) // second byte encodes the combined length of the R and S components
2023-12-13 10:56:33 +00:00
) ;
}
2024-02-24 22:47:50 +00:00
/ * *
* Validates most standardness rules
*
* returns true early if any standardness rule is violated , otherwise false
* ( except for non - mandatory - script - verify - flag and p2sh script evaluation rules which are * not * enforced )
* /
static isNonStandard ( tx : TransactionExtended ) : boolean {
// version
if ( tx . version > TX_MAX_STANDARD_VERSION ) {
return true ;
}
// tx-size
if ( tx . weight > MAX_STANDARD_TX_WEIGHT ) {
return true ;
}
// tx-size-small
if ( this . getNonWitnessSize ( tx ) < MIN_STANDARD_TX_NONWITNESS_SIZE ) {
return true ;
}
// bad-txns-too-many-sigops
if ( tx . sigops && tx . sigops > MAX_STANDARD_TX_SIGOPS_COST ) {
return true ;
}
// input validation
for ( const vin of tx . vin ) {
if ( vin . is_coinbase ) {
// standardness rules don't apply to coinbase transactions
return false ;
}
// scriptsig-size
if ( ( vin . scriptsig . length / 2 ) > MAX_STANDARD_SCRIPTSIG_SIZE ) {
return true ;
}
// scriptsig-not-pushonly
if ( vin . scriptsig_asm ) {
for ( const op of vin . scriptsig_asm . split ( ' ' ) ) {
if ( opcodes [ op ] && opcodes [ op ] > opcodes [ 'OP_16' ] ) {
return true ;
}
}
}
// bad-txns-nonstandard-inputs
if ( vin . prevout ? . scriptpubkey_type === 'p2sh' ) {
// TODO: evaluate script (https://github.com/bitcoin/bitcoin/blob/1ac627c485a43e50a9a49baddce186ee3ad4daad/src/policy/policy.cpp#L177)
// countScriptSigops returns the witness-scaled sigops, so divide by 4 before comparison with MAX_P2SH_SIGOPS
const sigops = ( transactionUtils . countScriptSigops ( vin . inner_redeemscript_asm ) / 4 ) ;
if ( sigops > MAX_P2SH_SIGOPS ) {
return true ;
}
} else if ( [ 'unknown' , 'provably_unspendable' , 'empty' ] . includes ( vin . prevout ? . scriptpubkey_type || '' ) ) {
return true ;
}
// TODO: bad-witness-nonstandard
}
// output validation
let opreturnCount = 0 ;
for ( const vout of tx . vout ) {
// scriptpubkey
2024-06-16 22:25:40 +00:00
if ( [ 'nonstandard' , 'provably_unspendable' , 'empty' ] . includes ( vout . scriptpubkey_type ) ) {
2024-02-24 22:47:50 +00:00
// (non-standard output type)
return true ;
2024-06-16 22:25:40 +00:00
} else if ( vout . scriptpubkey_type === 'unknown' ) {
// undefined segwit version/length combinations are actually standard in outputs
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/interpreter.cpp#L1950-L1951
if ( vout . scriptpubkey . startsWith ( '00' ) || ! this . isWitnessProgram ( vout . scriptpubkey ) ) {
return true ;
}
2024-02-24 22:47:50 +00:00
} else if ( vout . scriptpubkey_type === 'multisig' ) {
if ( ! DEFAULT_PERMIT_BAREMULTISIG ) {
// bare-multisig
return true ;
}
const mOfN = parseMultisigScript ( vout . scriptpubkey_asm ) ;
if ( ! mOfN || mOfN . n < 1 || mOfN . n > 3 || mOfN . m < 1 || mOfN . m > mOfN . n ) {
// (non-standard bare multisig threshold)
return true ;
}
} else if ( vout . scriptpubkey_type === 'op_return' ) {
opreturnCount ++ ;
if ( ( vout . scriptpubkey . length / 2 ) > MAX_OP_RETURN_RELAY ) {
// over default datacarrier limit
return true ;
}
}
// dust
// (we could probably hardcode this for the different output types...)
if ( vout . scriptpubkey_type !== 'op_return' ) {
let dustSize = ( vout . scriptpubkey . length / 2 ) ;
// add varint length overhead
dustSize += getVarIntLength ( dustSize ) ;
// add value size
dustSize += 8 ;
if ( [ 'v0_p2wpkh' , 'v0_p2wsh' , 'v1_p2tr' ] . includes ( vout . scriptpubkey_type ) ) {
dustSize += 67 ;
} else {
dustSize += 148 ;
}
if ( vout . value < ( dustSize * DUST_RELAY_TX_FEE ) ) {
// under minimum output size
return true ;
}
}
}
// multi-op-return
if ( opreturnCount > 1 ) {
return true ;
}
// TODO: non-mandatory-script-verify-flag
return false ;
}
2024-06-16 22:25:40 +00:00
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
// followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
static isWitnessProgram ( scriptpubkey : string ) : false | { version : number , program : string } {
if ( scriptpubkey . length < 8 || scriptpubkey . length > 84 ) {
return false ;
}
const version = parseInt ( scriptpubkey . slice ( 0 , 2 ) , 16 ) ;
if ( version !== 0 && version < 0x51 || version > 0x60 ) {
return false ;
}
const push = parseInt ( scriptpubkey . slice ( 2 , 4 ) , 16 ) ;
if ( push + 2 === ( scriptpubkey . length / 2 ) ) {
return {
version : version ? version - 0x50 : 0 ,
program : scriptpubkey.slice ( 4 ) ,
} ;
}
return false ;
}
2024-02-24 22:47:50 +00:00
static getNonWitnessSize ( tx : TransactionExtended ) : number {
let weight = tx . weight ;
let hasWitness = false ;
for ( const vin of tx . vin ) {
if ( vin . witness ? . length ) {
hasWitness = true ;
// witness count
weight -= getVarIntLength ( vin . witness . length ) ;
for ( const witness of vin . witness ) {
// witness item size + content
weight -= getVarIntLength ( witness . length / 2 ) + ( witness . length / 2 ) ;
}
}
}
if ( hasWitness ) {
// marker & segwit flag
weight -= 2 ;
}
return Math . ceil ( weight / 4 ) ;
}
2023-12-13 10:56:33 +00:00
static setSegwitSighashFlags ( flags : bigint , witness : string [ ] ) : bigint {
for ( const w of witness ) {
if ( this . isDERSig ( w ) ) {
flags |= this . setSighashFlags ( flags , w ) ;
}
}
return flags ;
}
static setLegacySighashFlags ( flags : bigint , scriptsig_asm : string ) : bigint {
for ( const item of scriptsig_asm . split ( ' ' ) ) {
// skip op_codes
if ( item . startsWith ( 'OP_' ) ) {
continue ;
}
// check pushed data
if ( this . isDERSig ( item ) ) {
flags |= this . setSighashFlags ( flags , item ) ;
}
}
return flags ;
}
2023-12-05 06:54:31 +00:00
static setSighashFlags ( flags : bigint , signature : string ) : bigint {
switch ( signature . slice ( - 2 ) ) {
case '01' : return flags | TransactionFlags . sighash_all ;
case '02' : return flags | TransactionFlags . sighash_none ;
case '03' : return flags | TransactionFlags . sighash_single ;
case '81' : return flags | TransactionFlags . sighash_all | TransactionFlags . sighash_acp ;
case '82' : return flags | TransactionFlags . sighash_none | TransactionFlags . sighash_acp ;
case '83' : return flags | TransactionFlags . sighash_single | TransactionFlags . sighash_acp ;
default : return flags | TransactionFlags . sighash_default ; // taproot only
}
}
2023-12-14 11:26:17 +00:00
static isBurnKey ( pubkey : string ) : boolean {
return [
'022222222222222222222222222222222222222222222222222222222222222222' ,
'033333333333333333333333333333333333333333333333333333333333333333' ,
'020202020202020202020202020202020202020202020202020202020202020202' ,
'030303030303030303030303030303030303030303030303030303030303030303' ,
] . includes ( pubkey ) ;
}
2024-04-04 12:32:03 +00:00
static isInscription ( vin , flags ) : bigint {
// in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin . witness ? . [ vin . witness . length - 1 ] . startsWith ( '50' ) ;
// script spends have more than one witness item, not counting the annex (if present)
if ( vin . witness . length > ( hasAnnex ? 2 : 1 ) ) {
// the script itself is the second-to-last witness item, not counting the annex
const asm = vin . inner_witnessscript_asm || transactionUtils . convertScriptSigAsm ( vin . witness [ vin . witness . length - ( hasAnnex ? 3 : 2 ) ] ) ;
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
if ( asm ? . includes ( 'OP_0 OP_IF' ) ) {
flags |= TransactionFlags . inscription ;
}
}
return flags ;
}
2023-12-05 06:54:31 +00:00
static getTransactionFlags ( tx : TransactionExtended ) : number {
2023-12-17 09:57:15 +00:00
let flags = tx . flags ? BigInt ( tx . flags ) : 0 n ;
// Update variable flags (CPFP, RBF)
if ( tx . ancestors ? . length ) {
flags |= TransactionFlags . cpfp_child ;
}
if ( tx . descendants ? . length ) {
flags |= TransactionFlags . cpfp_parent ;
}
2023-12-17 10:45:26 +00:00
if ( tx . replacement ) {
2023-12-17 09:57:15 +00:00
flags |= TransactionFlags . replacement ;
}
// Already processed static flags, no need to do it again
if ( tx . flags ) {
return Number ( flags ) ;
}
// Process static flags
2023-12-05 06:54:31 +00:00
if ( tx . version === 1 ) {
flags |= TransactionFlags . v1 ;
} else if ( tx . version === 2 ) {
flags |= TransactionFlags . v2 ;
2024-02-12 20:00:57 +00:00
} else if ( tx . version === 3 ) {
flags |= TransactionFlags . v3 ;
2023-12-05 06:54:31 +00:00
}
2024-02-04 21:47:31 +00:00
const reusedInputAddresses : { [ address : string ] : number } = { } ;
const reusedOutputAddresses : { [ address : string ] : number } = { } ;
2023-12-05 06:54:31 +00:00
const inValues = { } ;
const outValues = { } ;
let rbf = false ;
for ( const vin of tx . vin ) {
if ( vin . sequence < 0xfffffffe ) {
2023-12-13 10:56:33 +00:00
rbf = true ;
2023-12-05 06:54:31 +00:00
}
2024-04-04 12:32:03 +00:00
if ( vin . prevout ? . scriptpubkey_type ) {
switch ( vin . prevout ? . scriptpubkey_type ) {
case 'p2pk' : flags |= TransactionFlags . p2pk ; break ;
case 'multisig' : flags |= TransactionFlags . p2ms ; break ;
case 'p2pkh' : flags |= TransactionFlags . p2pkh ; break ;
case 'p2sh' : flags |= TransactionFlags . p2sh ; break ;
case 'v0_p2wpkh' : flags |= TransactionFlags . p2wpkh ; break ;
case 'v0_p2wsh' : flags |= TransactionFlags . p2wsh ; break ;
case 'v1_p2tr' : {
if ( ! vin . witness ? . length ) {
throw new Error ( 'Taproot input missing witness data' ) ;
2023-12-05 06:54:31 +00:00
}
2024-04-04 12:32:03 +00:00
flags |= TransactionFlags . p2tr ;
flags = Common . isInscription ( vin , flags ) ;
} break ;
}
} else {
// no prevouts, optimistically check witness-bearing inputs
if ( vin . witness ? . length >= 2 ) {
try {
flags = Common . isInscription ( vin , flags ) ;
} catch {
// witness script parsing will fail if this isn't really a taproot output
2023-12-05 06:54:31 +00:00
}
2024-04-04 12:32:03 +00:00
}
2023-12-05 06:54:31 +00:00
}
2023-12-13 10:56:33 +00:00
// sighash flags
if ( vin . prevout ? . scriptpubkey_type === 'v1_p2tr' ) {
flags |= this . setSchnorrSighashFlags ( flags , vin . witness ) ;
} else if ( vin . witness ) {
flags |= this . setSegwitSighashFlags ( flags , vin . witness ) ;
2023-12-14 11:26:17 +00:00
} else if ( vin . scriptsig ? . length ) {
flags |= this . setLegacySighashFlags ( flags , vin . scriptsig_asm || transactionUtils . convertScriptSigAsm ( vin . scriptsig ) ) ;
2023-12-13 10:56:33 +00:00
}
if ( vin . prevout ? . scriptpubkey_address ) {
2024-02-04 21:47:31 +00:00
reusedInputAddresses [ vin . prevout ? . scriptpubkey_address ] = ( reusedInputAddresses [ vin . prevout ? . scriptpubkey_address ] || 0 ) + 1 ;
2023-12-13 10:56:33 +00:00
}
2023-12-05 06:54:31 +00:00
inValues [ vin . prevout ? . value || Math . random ( ) ] = ( inValues [ vin . prevout ? . value || Math . random ( ) ] || 0 ) + 1 ;
}
if ( rbf ) {
flags |= TransactionFlags . rbf ;
} else {
flags |= TransactionFlags . no_rbf ;
}
2023-12-14 11:26:17 +00:00
let hasFakePubkey = false ;
2024-03-08 00:48:36 +00:00
let P2WSHCount = 0 ;
let olgaSize = 0 ;
2023-12-05 06:54:31 +00:00
for ( const vout of tx . vout ) {
switch ( vout . scriptpubkey_type ) {
2023-12-14 11:26:17 +00:00
case 'p2pk' : {
flags |= TransactionFlags . p2pk ;
// detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve)
2024-01-30 16:41:35 +00:00
hasFakePubkey = hasFakePubkey || ! isPoint ( vout . scriptpubkey ? . slice ( 2 , - 2 ) ) ;
2023-12-14 11:26:17 +00:00
} break ;
2023-12-05 06:54:31 +00:00
case 'multisig' : {
flags |= TransactionFlags . p2ms ;
2023-12-14 11:26:17 +00:00
// detect fake pubkeys (i.e. not valid DER points on the secp256k1 curve)
const asm = vout . scriptpubkey_asm || transactionUtils . convertScriptSigAsm ( vout . scriptpubkey ) ;
for ( const key of ( asm ? . split ( ' ' ) || [ ] ) ) {
if ( ! hasFakePubkey && ! key . startsWith ( 'OP_' ) ) {
hasFakePubkey = hasFakePubkey || this . isBurnKey ( key ) || ! isPoint ( key ) ;
}
}
2023-12-05 06:54:31 +00:00
} break ;
case 'p2pkh' : flags |= TransactionFlags . p2pkh ; break ;
case 'p2sh' : flags |= TransactionFlags . p2sh ; break ;
case 'v0_p2wpkh' : flags |= TransactionFlags . p2wpkh ; break ;
case 'v0_p2wsh' : flags |= TransactionFlags . p2wsh ; break ;
case 'v1_p2tr' : flags |= TransactionFlags . p2tr ; break ;
case 'op_return' : flags |= TransactionFlags . op_return ; break ;
}
2023-12-13 10:56:33 +00:00
if ( vout . scriptpubkey_address ) {
2024-02-04 21:47:31 +00:00
reusedOutputAddresses [ vout . scriptpubkey_address ] = ( reusedOutputAddresses [ vout . scriptpubkey_address ] || 0 ) + 1 ;
2023-12-13 10:56:33 +00:00
}
2024-03-08 00:48:36 +00:00
if ( vout . scriptpubkey_type === 'v0_p2wsh' ) {
if ( ! P2WSHCount ) {
olgaSize = parseInt ( vout . scriptpubkey . slice ( 4 , 8 ) , 16 ) ;
}
P2WSHCount ++ ;
if ( P2WSHCount === Math . ceil ( ( olgaSize + 2 ) / 32 ) ) {
const nullBytes = ( P2WSHCount * 32 ) - olgaSize - 2 ;
if ( vout . scriptpubkey . endsWith ( '' . padEnd ( nullBytes * 2 , '0' ) ) ) {
flags |= TransactionFlags . fake_scripthash ;
}
}
} else {
P2WSHCount = 0 ;
}
2023-12-05 06:54:31 +00:00
outValues [ vout . value || Math . random ( ) ] = ( outValues [ vout . value || Math . random ( ) ] || 0 ) + 1 ;
}
2023-12-14 11:26:17 +00:00
if ( hasFakePubkey ) {
flags |= TransactionFlags . fake_pubkey ;
}
2023-12-17 09:57:15 +00:00
2023-12-05 06:54:31 +00:00
// fast but bad heuristic to detect possible coinjoins
2023-12-13 10:56:33 +00:00
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
2024-02-04 21:47:31 +00:00
const addressReuse = Object . keys ( reusedOutputAddresses ) . reduce ( ( acc , key ) = > Math . max ( acc , ( reusedInputAddresses [ key ] || 0 ) + ( reusedOutputAddresses [ key ] || 0 ) ) , 0 ) > 1 ;
2023-12-13 10:56:33 +00:00
if ( ! addressReuse && tx . vin . length >= 5 && tx . vout . length >= 5 && ( Object . keys ( inValues ) . length + Object . keys ( outValues ) . length ) <= ( tx . vin . length + tx . vout . length ) / 2 ) {
2023-12-05 06:54:31 +00:00
flags |= TransactionFlags . coinjoin ;
}
// more than 5:1 input:output ratio
if ( tx . vin . length / tx . vout . length >= 5 ) {
flags |= TransactionFlags . consolidation ;
}
// less than 1:5 input:output ratio
if ( tx . vin . length / tx . vout . length <= 0.2 ) {
flags |= TransactionFlags . batch_payout ;
}
2024-02-24 22:47:50 +00:00
if ( this . isNonStandard ( tx ) ) {
flags |= TransactionFlags . nonstandard ;
}
2023-12-05 06:54:31 +00:00
return Number ( flags ) ;
}
static classifyTransaction ( tx : TransactionExtended ) : TransactionClassified {
2024-01-30 16:41:35 +00:00
let flags = 0 ;
try {
flags = Common . getTransactionFlags ( tx ) ;
} catch ( e ) {
logger . warn ( 'Failed to add classification flags to transaction: ' + ( e instanceof Error ? e.message : e ) ) ;
}
2023-12-17 09:57:15 +00:00
tx . flags = flags ;
2023-12-05 06:54:31 +00:00
return {
2023-12-21 16:20:31 +00:00
. . . Common . stripTransaction ( tx ) ,
2023-12-05 06:54:31 +00:00
flags ,
} ;
}
2023-12-21 16:20:31 +00:00
static classifyTransactions ( txs : TransactionExtended [ ] ) : TransactionClassified [ ] {
return txs . map ( Common . classifyTransaction ) ;
}
2020-09-26 02:11:30 +07:00
static stripTransaction ( tx : TransactionExtended ) : TransactionStripped {
return {
txid : tx.txid ,
2023-08-01 17:33:03 +09:00
fee : tx.fee || 0 ,
2021-01-25 01:09:42 +07:00
vsize : tx.weight / 4 ,
2020-12-21 23:08:34 +07:00
value : tx.vout.reduce ( ( acc , vout ) = > acc + ( vout . value ? vout.value : 0 ) , 0 ) ,
2023-05-26 21:10:32 -04:00
acc : tx.acceleration || undefined ,
2023-03-14 15:39:55 +09:00
rate : tx.effectiveFeePerVsize ,
2024-03-31 03:45:48 +00:00
time : tx.firstSeen || undefined ,
2020-09-26 02:11:30 +07:00
} ;
}
2020-12-28 20:17:32 +07:00
2023-06-29 19:24:19 -04:00
static stripTransactions ( txs : TransactionExtended [ ] ) : TransactionStripped [ ] {
2023-12-21 16:20:31 +00:00
return txs . map ( Common . stripTransaction ) ;
2023-06-29 19:24:19 -04:00
}
2022-04-22 04:03:08 -04:00
static sleep $ ( ms : number ) : Promise < void > {
2020-12-28 20:17:32 +07:00
return new Promise ( ( resolve ) = > {
setTimeout ( ( ) = > {
resolve ( ) ;
} , ms ) ;
} ) ;
}
2021-04-10 21:26:05 +04:00
2021-03-19 13:47:37 +07:00
static shuffleArray ( array : any [ ] ) {
for ( let i = array . length - 1 ; i > 0 ; i -- ) {
const j = Math . floor ( Math . random ( ) * ( i + 1 ) ) ;
[ array [ i ] , array [ j ] ] = [ array [ j ] , array [ i ] ] ;
}
}
2021-03-18 23:47:40 +07:00
2023-03-14 14:52:34 +09:00
// calculates the ratio of matched transactions to projected transactions by weight
static getSimilarity ( projectedBlock : MempoolBlockWithTransactions , transactions : TransactionExtended [ ] ) : number {
let matchedWeight = 0 ;
let projectedWeight = 0 ;
const inBlock = { } ;
for ( const tx of transactions ) {
inBlock [ tx . txid ] = tx ;
}
// look for transactions that were expected in the template, but missing from the mined block
for ( const tx of projectedBlock . transactions ) {
if ( inBlock [ tx . txid ] ) {
matchedWeight += tx . vsize * 4 ;
}
projectedWeight += tx . vsize * 4 ;
}
projectedWeight += transactions [ 0 ] . weight ;
matchedWeight += transactions [ 0 ] . weight ;
return projectedWeight ? matchedWeight / projectedWeight : 1 ;
}
2022-02-09 19:41:05 +09:00
static getSqlInterval ( interval : string | null ) : string | null {
switch ( interval ) {
case '24h' : return '1 DAY' ;
case '3d' : return '3 DAY' ;
case '1w' : return '1 WEEK' ;
case '1m' : return '1 MONTH' ;
case '3m' : return '3 MONTH' ;
case '6m' : return '6 MONTH' ;
case '1y' : return '1 YEAR' ;
case '2y' : return '2 YEAR' ;
case '3y' : return '3 YEAR' ;
2023-03-04 18:48:16 +09:00
case '4y' : return '4 YEAR' ;
2022-02-09 19:41:05 +09:00
default : return null ;
}
}
2022-04-25 15:50:26 +09:00
2022-02-15 16:02:30 +09:00
static indexingEnabled ( ) : boolean {
return (
2022-07-11 08:41:28 +02:00
[ 'mainnet' , 'testnet' , 'signet' ] . includes ( config . MEMPOOL . NETWORK ) &&
2022-02-15 16:02:30 +09:00
config . DATABASE . ENABLED === true &&
2022-04-20 13:12:32 +09:00
config . MEMPOOL . INDEXING_BLOCKS_AMOUNT !== 0
2022-02-15 16:02:30 +09:00
) ;
}
2022-06-18 16:48:02 +02:00
static blocksSummariesIndexingEnabled ( ) : boolean {
return (
Common . indexingEnabled ( ) &&
config . MEMPOOL . BLOCKS_SUMMARIES_INDEXING === true
) ;
}
2022-08-08 09:00:11 +02:00
2024-01-27 18:22:58 +00:00
static gogglesIndexingEnabled ( ) : boolean {
return (
Common . blocksSummariesIndexingEnabled ( ) &&
config . MEMPOOL . GOGGLES_INDEXING === true
) ;
}
2022-11-27 13:46:23 +09:00
static cpfpIndexingEnabled ( ) : boolean {
return (
Common . indexingEnabled ( ) &&
2022-12-28 05:28:37 -06:00
config . MEMPOOL . CPFP_INDEXING === true
2022-11-27 13:46:23 +09:00
) ;
}
2022-08-08 09:00:11 +02:00
static setDateMidnight ( date : Date ) : void {
date . setUTCHours ( 0 ) ;
date . setUTCMinutes ( 0 ) ;
date . setUTCSeconds ( 0 ) ;
date . setUTCMilliseconds ( 0 ) ;
}
2022-08-10 16:58:29 +02:00
static channelShortIdToIntegerId ( channelId : string ) : string {
if ( channelId . indexOf ( 'x' ) === - 1 ) { // Already an integer id
return channelId ;
}
if ( channelId . indexOf ( '/' ) !== - 1 ) { // Topology import
channelId = channelId . slice ( 0 , - 2 ) ;
2022-08-08 09:00:11 +02:00
}
2022-08-10 16:58:29 +02:00
const s = channelId . split ( 'x' ) . map ( part = > BigInt ( part ) ) ;
return ( ( s [ 0 ] << 40 n ) | ( s [ 1 ] << 16 n ) | s [ 2 ] ) . toString ( ) ;
2022-08-08 09:00:11 +02:00
}
/** Decodes a channel id returned by lnd as uint64 to a short channel id */
static channelIntegerIdToShortId ( id : string ) : string {
2022-08-18 10:59:03 +02:00
if ( id . indexOf ( '/' ) !== - 1 ) {
id = id . slice ( 0 , - 2 ) ;
}
2022-08-10 16:58:29 +02:00
if ( id . indexOf ( 'x' ) !== - 1 ) { // Already a short id
return id ;
}
2022-08-08 09:00:11 +02:00
const n = BigInt ( id ) ;
return [
n >> 40 n , // nth block
( n >> 16 n ) & 0xffffff n , // nth tx of the block
n & 0xffff n // nth output of the tx
] . join ( 'x' ) ;
}
2023-02-13 14:23:32 +09:00
static utcDateToMysql ( date? : number | null ) : string | null {
if ( date === null ) {
return null ;
}
2022-08-08 09:00:11 +02:00
const d = new Date ( ( date || 0 ) * 1000 ) ;
return d . toISOString ( ) . split ( 'T' ) [ 0 ] + ' ' + d . toTimeString ( ) . split ( ' ' ) [ 0 ] ;
}
2022-08-13 10:24:11 +02:00
2022-08-29 08:43:26 +02:00
static findSocketNetwork ( addr : string ) : { network : string | null , url : string } {
2022-08-13 10:24:11 +02:00
let network : string | null = null ;
2023-02-13 18:01:15 +09:00
let url : string = addr ;
if ( config . LIGHTNING . BACKEND === 'cln' ) {
url = addr . split ( '://' ) [ 1 ] ;
}
2022-08-13 10:24:11 +02:00
2022-08-29 08:43:26 +02:00
if ( ! url ) {
return {
network : null ,
url : addr ,
} ;
}
if ( addr . indexOf ( 'onion' ) !== - 1 ) {
if ( url . split ( '.' ) [ 0 ] . length >= 56 ) {
network = 'torv3' ;
2022-08-13 10:24:11 +02:00
} else {
2022-08-29 08:43:26 +02:00
network = 'torv2' ;
}
} else if ( addr . indexOf ( 'i2p' ) !== - 1 ) {
network = 'i2p' ;
2023-02-13 18:01:15 +09:00
} else if ( addr . indexOf ( 'ipv4' ) !== - 1 || ( config . LIGHTNING . BACKEND === 'lnd' && isIP ( url . split ( ':' ) [ 0 ] ) === 4 ) ) {
2022-08-29 08:43:26 +02:00
const ipv = isIP ( url . split ( ':' ) [ 0 ] ) ;
if ( ipv === 4 ) {
network = 'ipv4' ;
} else {
return {
network : null ,
url : addr ,
} ;
2022-08-13 10:24:11 +02:00
}
2023-02-13 18:01:15 +09:00
} else if ( addr . indexOf ( 'ipv6' ) !== - 1 || ( config . LIGHTNING . BACKEND === 'lnd' && url . indexOf ( ']:' ) ) ) {
2022-08-29 08:43:26 +02:00
url = url . split ( '[' ) [ 1 ] . split ( ']' ) [ 0 ] ;
const ipv = isIP ( url ) ;
if ( ipv === 6 ) {
const parts = addr . split ( ':' ) ;
network = 'ipv6' ;
url = ` [ ${ url } ]: ${ parts [ parts . length - 1 ] } ` ;
} else {
return {
network : null ,
url : addr ,
} ;
}
} else {
return {
network : null ,
url : addr ,
} ;
2022-08-13 10:24:11 +02:00
}
return {
network : network ,
2022-08-29 08:43:26 +02:00
url : url ,
2022-08-13 10:24:11 +02:00
} ;
}
2022-08-29 08:43:26 +02:00
static formatSocket ( publicKey : string , socket : { network : string , addr : string } ) : NodeSocket {
if ( config . LIGHTNING . BACKEND === 'cln' ) {
return {
publicKey : publicKey ,
network : socket.network ,
addr : socket.addr ,
} ;
} else /* if (config.LIGHTNING.BACKEND === 'lnd') */ {
const formatted = this . findSocketNetwork ( socket . addr ) ;
return {
publicKey : publicKey ,
network : formatted.network ,
addr : formatted.url ,
} ;
}
}
2023-03-12 14:36:36 +09:00
2024-01-23 00:44:34 +00:00
static calculateCpfp ( height : number , transactions : TransactionExtended [ ] , saveRelatives : boolean = false ) : CpfpSummary {
2023-06-06 14:42:41 -04:00
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
2024-01-23 00:44:34 +00:00
const txMap : { [ txid : string ] : TransactionExtended } = { } ;
2023-06-06 14:42:41 -04:00
// initialize the txMap
for ( const tx of transactions ) {
txMap [ tx . txid ] = tx ;
}
// reverse pass to identify CPFP clusters
2023-03-12 14:36:36 +09:00
for ( let i = transactions . length - 1 ; i >= 0 ; i -- ) {
const tx = transactions [ i ] ;
if ( ! ancestors [ tx . txid ] ) {
let totalFee = 0 ;
let totalVSize = 0 ;
2023-06-06 14:42:41 -04:00
clusterTxs . forEach ( tx = > {
2023-03-12 14:36:36 +09:00
totalFee += tx ? . fee || 0 ;
totalVSize += ( tx . weight / 4 ) ;
} ) ;
const effectiveFeePerVsize = totalFee / totalVSize ;
2023-06-06 14:42:41 -04:00
let cluster : CpfpCluster ;
if ( clusterTxs . length > 1 ) {
cluster = {
root : clusterTxs [ 0 ] . txid ,
2023-03-12 14:36:36 +09:00
height ,
2023-06-06 14:42:41 -04:00
txs : clusterTxs.map ( tx = > { return { txid : tx.txid , weight : tx.weight , fee : tx.fee || 0 } ; } ) ,
2023-03-12 14:36:36 +09:00
effectiveFeePerVsize ,
2023-06-06 14:42:41 -04:00
} ;
clusters . push ( cluster ) ;
2023-03-12 14:36:36 +09:00
}
2023-06-06 14:42:41 -04:00
clusterTxs . forEach ( tx = > {
2023-03-12 14:36:36 +09:00
txMap [ tx . txid ] . effectiveFeePerVsize = effectiveFeePerVsize ;
2023-06-06 14:42:41 -04:00
if ( cluster ) {
clusterMap [ tx . txid ] = cluster ;
}
2023-03-12 14:36:36 +09:00
} ) ;
2023-06-06 14:42:41 -04:00
// reset working vars
clusterTxs = [ ] ;
2023-03-12 14:36:36 +09:00
ancestors = { } ;
}
2023-06-06 14:42:41 -04:00
clusterTxs . push ( tx ) ;
2023-03-12 14:36:36 +09:00
tx . vin . forEach ( vin = > {
ancestors [ vin . txid ] = true ;
} ) ;
}
2023-06-06 14:42:41 -04:00
// 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 ;
}
}
}
2024-01-23 00:44:34 +00:00
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 ;
} ) ;
}
}
2023-03-12 14:36:36 +09:00
return {
transactions ,
clusters ,
} ;
}
2023-07-18 16:08:25 +09:00
static calcEffectiveFeeStatistics ( transactions : { weight : number , fee : number , effectiveFeePerVsize? : number , txid : string , acceleration? : boolean } [ ] ) : EffectiveFeeStats {
2023-03-12 14:36:36 +09:00
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 ) ;
let weightCount = 0 ;
let medianFee = 0 ;
let medianWeight = 0 ;
2024-06-22 04:38:06 +00:00
// calculate the "medianFee" as the average fee rate of the middle 0.25% weight units of transactions
const halfWidth = config . MEMPOOL . BLOCK_WEIGHT_UNITS / 800 ;
const leftBound = Math . floor ( ( config . MEMPOOL . BLOCK_WEIGHT_UNITS / 2 ) - halfWidth ) ;
const rightBound = Math . ceil ( ( config . MEMPOOL . BLOCK_WEIGHT_UNITS / 2 ) + halfWidth ) ;
2023-03-12 14:36:36 +09:00
for ( let i = 0 ; i < sortedTxs . length && weightCount < rightBound ; i ++ ) {
const left = weightCount ;
const right = weightCount + sortedTxs [ i ] . weight ;
if ( right > leftBound ) {
const weight = Math . min ( right , rightBound ) - Math . max ( left , leftBound ) ;
medianFee += ( sortedTxs [ i ] . rate * ( weight / 4 ) ) ;
medianWeight += weight ;
}
weightCount += sortedTxs [ i ] . weight ;
}
const medianFeeRate = medianWeight ? ( medianFee / ( medianWeight / 4 ) ) : 0 ;
// minimum effective fee heuristic:
// lowest of
// a) the 1st percentile of effective fee rates
// b) the minimum effective fee rate in the last 2% of transactions (in block order)
const minFee = Math . min (
Common . getNthPercentile ( 1 , sortedTxs ) . rate ,
transactions . slice ( - transactions . length / 50 ) . reduce ( ( min , tx ) = > { return Math . min ( min , tx . effectiveFeePerVsize || ( ( tx . fee || 0 ) / ( tx . weight / 4 ) ) ) ; } , Infinity )
) ;
// maximum effective fee heuristic:
// highest of
// a) the 99th percentile of effective fee rates
// b) the maximum effective fee rate in the first 2% of transactions (in block order)
const maxFee = Math . max (
Common . getNthPercentile ( 99 , sortedTxs ) . rate ,
transactions . slice ( 0 , transactions . length / 50 ) . reduce ( ( max , tx ) = > { return Math . max ( max , tx . effectiveFeePerVsize || ( ( tx . fee || 0 ) / ( tx . weight / 4 ) ) ) ; } , 0 )
) ;
return {
medianFee : medianFeeRate ,
feeRange : [
minFee ,
[ 10 , 25 , 50 , 75 , 90 ] . map ( n = > Common . getNthPercentile ( n , sortedTxs ) . rate ) ,
maxFee ,
] . flat ( ) ,
} ;
}
static getNthPercentile ( n : number , sortedDistribution : any [ ] ) : any {
return sortedDistribution [ Math . floor ( ( sortedDistribution . length - 1 ) * ( n / 100 ) ) ] ;
}
2023-04-19 18:10:10 -07:00
static getTransactionFromRequest ( req : Request , form : boolean ) : string {
let rawTx : any = typeof req . body === 'object' && form
? Object . values ( req . body ) [ 0 ] as any
: req . body ;
if ( typeof rawTx !== 'string' ) {
throw Object . assign ( new Error ( 'Non-string request body' ) , { code : - 1 } ) ;
}
// Support both upper and lower case hex
// Support both txHash= Form and direct API POST
const reg = form ? /^txHash=((?:[a-fA-F0-9]{2})+)$/ : /^((?:[a-fA-F0-9]{2})+)$/ ;
const matches = reg . exec ( rawTx ) ;
if ( ! matches || ! matches [ 1 ] ) {
throw Object . assign ( new Error ( 'Non-hex request body' ) , { code : - 2 } ) ;
}
// Guaranteed to be a hex string of multiple of 2
// Guaranteed to be lower case
// Guaranteed to pass validation (see function below)
return this . validateTransactionHex ( matches [ 1 ] . toLowerCase ( ) ) ;
}
2024-03-25 05:52:03 +00:00
static getTransactionsFromRequest ( req : Request , limit : number = 25 ) : string [ ] {
if ( ! Array . isArray ( req . body ) || req . body . some ( hex = > typeof hex !== 'string' ) ) {
throw Object . assign ( new Error ( 'Invalid request body (should be an array of hexadecimal strings)' ) , { code : - 1 } ) ;
}
if ( limit && req . body . length > limit ) {
throw Object . assign ( new Error ( 'Exceeded maximum of 25 transactions' ) , { code : - 1 } ) ;
2024-03-23 11:27:28 +00:00
}
2024-03-25 05:52:03 +00:00
const txs = req . body ;
2024-03-23 11:27:28 +00:00
return txs . map ( rawTx = > {
// Support both upper and lower case hex
// Support both txHash= Form and direct API POST
const reg = /^((?:[a-fA-F0-9]{2})+)$/ ;
const matches = reg . exec ( rawTx ) ;
if ( ! matches || ! matches [ 1 ] ) {
throw Object . assign ( new Error ( 'Invalid hex string' ) , { code : - 2 } ) ;
}
// Guaranteed to be a hex string of multiple of 2
// Guaranteed to be lower case
// Guaranteed to pass validation (see function below)
return this . validateTransactionHex ( matches [ 1 ] . toLowerCase ( ) ) ;
} ) ;
}
2023-04-19 18:10:10 -07:00
private static validateTransactionHex ( txhex : string ) : string {
// Do not mutate txhex
// We assume txhex to be valid hex (output of getTransactionFromRequest above)
// Check 1: Valid transaction parse
let tx : bitcoinjs.Transaction ;
try {
tx = bitcoinjs . Transaction . fromHex ( txhex ) ;
} catch ( e ) {
throw Object . assign ( new Error ( 'Invalid transaction (could not parse)' ) , { code : - 4 } ) ;
}
// Check 2: Simple size check
if ( tx . weight ( ) > config . MEMPOOL . MAX_PUSH_TX_SIZE_WEIGHT ) {
throw Object . assign ( new Error ( ` Transaction too large (max ${ config . MEMPOOL . MAX_PUSH_TX_SIZE_WEIGHT } weight units) ` ) , { code : - 3 } ) ;
}
// Check 3: Check unreachable script in taproot (if not allowed)
if ( ! config . MEMPOOL . ALLOW_UNREACHABLE ) {
tx . ins . forEach ( input = > {
const witness = input . witness ;
// See BIP 341: Script validation rules
const hasAnnex = witness . length >= 2 &&
witness [ witness . length - 1 ] [ 0 ] === 0x50 ;
const scriptSpendMinLength = hasAnnex ? 3 : 2 ;
const maybeScriptSpend = witness . length >= scriptSpendMinLength ;
if ( maybeScriptSpend ) {
const controlBlock = witness [ witness . length - scriptSpendMinLength + 1 ] ;
2023-07-13 13:50:54 +09:00
if ( controlBlock . length === 0 || ! this . isValidLeafVersion ( controlBlock [ 0 ] ) ) {
2023-04-19 18:10:10 -07:00
// Skip this input, it's not taproot
return ;
}
// Definitely taproot. Get script
const script = witness [ witness . length - scriptSpendMinLength ] ;
const decompiled = bitcoinjs . script . decompile ( script ) ;
if ( ! decompiled || decompiled . length < 2 ) {
// Skip this input
return ;
}
// Iterate up to second last (will look ahead 1 item)
for ( let i = 0 ; i < decompiled . length - 1 ; i ++ ) {
const first = decompiled [ i ] ;
const second = decompiled [ i + 1 ] ;
if (
first === bitcoinjs . opcodes . OP_FALSE &&
second === bitcoinjs . opcodes . OP_IF
) {
throw Object . assign ( new Error ( 'Unreachable taproot scripts not allowed' ) , { code : - 5 } ) ;
}
}
}
} )
}
// Pass through the input string untouched
return txhex ;
}
2023-07-13 13:50:54 +09:00
private static isValidLeafVersion ( leafVersion : number ) : boolean {
// See Note 7 in BIP341
// https://github.com/bitcoin/bips/blob/66a1a8151021913047934ebab3f8883f2f8ca75b/bip-0341.mediawiki#cite_note-7
// "What constraints are there on the leaf version?"
// Must be an integer between 0 and 255
// Since we're parsing a byte
if ( Math . floor ( leafVersion ) !== leafVersion || leafVersion < 0 || leafVersion > 255 ) {
return false ;
}
// "the leaf version cannot be odd"
if ( ( leafVersion & 0x01 ) === 1 ) {
return false ;
}
// "The values that comply to this rule are
// the 32 even values between 0xc0 and 0xfe
if ( leafVersion >= 0xc0 && leafVersion <= 0xfe ) {
return true ;
}
// and also 0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe."
if ( [ 0x66 , 0x7e , 0x80 , 0x84 , 0x96 , 0x98 , 0xba , 0xbc , 0xbe ] . includes ( leafVersion ) ) {
return true ;
}
// Otherwise, invalid
return false ;
}
2020-05-24 16:29:30 +07:00
}
2023-05-10 12:59:05 -06:00
/ * *
* Class to calculate average fee rates of a list of transactions
* at certain weight percentiles , in a single pass
*
* init with :
* maxWeight - the total weight to measure percentiles relative to ( e . g . 4 MW for a single block )
* percentileBandWidth - how many weight units to average over for each percentile ( as a % of maxWeight )
* percentiles - an array of weight percentiles to compute , in %
*
* then call . processNext ( tx ) for each transaction , in descending order
*
* retrieve the final results with . getFeeStats ( )
* /
export class OnlineFeeStatsCalculator {
private maxWeight : number ;
private percentiles = [ 10 , 25 , 50 , 75 , 90 ] ;
private bandWidthPercent = 2 ;
private bandWidth : number = 0 ;
private bandIndex = 0 ;
private leftBound = 0 ;
private rightBound = 0 ;
private inBand = false ;
private totalBandFee = 0 ;
private totalBandWeight = 0 ;
private minBandRate = Infinity ;
private maxBandRate = 0 ;
private feeRange : { avg : number , min : number , max : number } [ ] = [ ] ;
private totalWeight : number = 0 ;
constructor ( maxWeight : number , percentileBandWidth? : number , percentiles? : number [ ] ) {
this . maxWeight = maxWeight ;
if ( percentiles && percentiles . length ) {
this . percentiles = percentiles ;
}
if ( percentileBandWidth != null ) {
this . bandWidthPercent = percentileBandWidth ;
}
this . bandWidth = this . maxWeight * ( this . bandWidthPercent / 100 ) ;
// add min/max percentiles aligned to the ends of the range
this . percentiles . unshift ( this . bandWidthPercent / 2 ) ;
this . percentiles . push ( 100 - ( this . bandWidthPercent / 2 ) ) ;
this . setNextBounds ( ) ;
}
processNext ( tx : { weight : number , fee : number , effectiveFeePerVsize? : number , feePerVsize? : number , rate? : number , txid : string } ) : void {
let left = this . totalWeight ;
const right = this . totalWeight + tx . weight ;
if ( ! this . inBand && right <= this . leftBound ) {
this . totalWeight += tx . weight ;
return ;
}
while ( left < right ) {
if ( right > this . leftBound ) {
this . inBand = true ;
const txRate = ( tx . rate || tx . effectiveFeePerVsize || tx . feePerVsize || 0 ) ;
const weight = Math . min ( right , this . rightBound ) - Math . max ( left , this . leftBound ) ;
this . totalBandFee += ( txRate * weight ) ;
this . totalBandWeight += weight ;
this . maxBandRate = Math . max ( this . maxBandRate , txRate ) ;
this . minBandRate = Math . min ( this . minBandRate , txRate ) ;
}
left = Math . min ( right , this . rightBound ) ;
if ( left >= this . rightBound ) {
this . inBand = false ;
const avgBandFeeRate = this . totalBandWeight ? ( this . totalBandFee / this . totalBandWeight ) : 0 ;
this . feeRange . unshift ( { avg : avgBandFeeRate , min : this.minBandRate , max : this.maxBandRate } ) ;
this . bandIndex ++ ;
this . setNextBounds ( ) ;
this . totalBandFee = 0 ;
this . totalBandWeight = 0 ;
this . minBandRate = Infinity ;
this . maxBandRate = 0 ;
}
}
this . totalWeight += tx . weight ;
}
private setNextBounds ( ) : void {
const nextPercentile = this . percentiles [ this . bandIndex ] ;
if ( nextPercentile != null ) {
this . leftBound = ( ( nextPercentile / 100 ) * this . maxWeight ) - ( this . bandWidth / 2 ) ;
this . rightBound = this . leftBound + this . bandWidth ;
} else {
this . leftBound = Infinity ;
this . rightBound = Infinity ;
}
}
getRawFeeStats ( ) : WorkingEffectiveFeeStats {
if ( this . totalBandWeight > 0 ) {
const avgBandFeeRate = this . totalBandWeight ? ( this . totalBandFee / this . totalBandWeight ) : 0 ;
this . feeRange . unshift ( { avg : avgBandFeeRate , min : this.minBandRate , max : this.maxBandRate } ) ;
}
while ( this . feeRange . length < this . percentiles . length ) {
this . feeRange . unshift ( { avg : 0 , min : 0 , max : 0 } ) ;
}
return {
minFee : this.feeRange [ 0 ] . min ,
medianFee : this.feeRange [ Math . floor ( this . feeRange . length / 2 ) ] . avg ,
maxFee : this.feeRange [ this . feeRange . length - 1 ] . max ,
feeRange : this.feeRange.map ( f = > f . avg ) ,
} ;
}
getFeeStats ( ) : EffectiveFeeStats {
const stats = this . getRawFeeStats ( ) ;
stats . feeRange [ 0 ] = stats . minFee ;
stats . feeRange [ stats . feeRange . length - 1 ] = stats . maxFee ;
return stats ;
}
}