2020-10-19 11:57:02 +07:00
import config from '../config' ;
2020-12-20 22:36:36 +07:00
import bitcoinApi from './bitcoin/bitcoin-api-factory' ;
2023-05-31 11:37:13 -04:00
import { MempoolTransactionExtended , TransactionExtended , VbytesPerSecond } from '../mempool.interfaces' ;
2020-10-13 15:27:52 +07:00
import logger from '../logger' ;
2020-09-26 02:11:30 +07:00
import { Common } from './common' ;
2020-12-21 23:08:34 +07:00
import transactionUtils from './transaction-utils' ;
2020-12-28 04:47:22 +07:00
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface' ;
2021-01-05 18:57:06 +07:00
import loadingIndicators from './loading-indicators' ;
2021-09-15 01:47:24 +04:00
import bitcoinClient from './bitcoin/bitcoin-client' ;
import bitcoinSecondClient from './bitcoin/bitcoin-second-client' ;
2022-03-08 14:49:25 +01:00
import rbfCache from './rbf-cache' ;
2019-07-21 17:59:47 +03:00
class Mempool {
2020-04-01 20:06:44 +07:00
private inSync : boolean = false ;
2022-02-11 21:25:58 +09:00
private mempoolCacheDelta : number = - 1 ;
2023-05-29 15:56:29 -04:00
private mempoolCache : { [ txId : string ] : MempoolTransactionExtended } = { } ;
private spendMap = new Map < string , MempoolTransactionExtended > ( ) ;
2022-02-13 13:52:04 +01:00
private mempoolInfo : IBitcoinApi.MempoolInfo = { loaded : false , size : 0 , bytes : 0 , usage : 0 , total_fee : 0 ,
2021-04-06 11:07:38 +04:00
maxmempool : 300000000 , mempoolminfee : 0.00001000 , minrelaytxfee : 0.00001000 } ;
2023-05-29 15:56:29 -04:00
private mempoolChangedCallback : ( ( newMempool : { [ txId : string ] : MempoolTransactionExtended ; } , newTransactions : MempoolTransactionExtended [ ] ,
deletedTransactions : MempoolTransactionExtended [ ] ) = > void ) | undefined ;
private $asyncMempoolChangedCallback : ( ( newMempool : { [ txId : string ] : MempoolTransactionExtended ; } , newTransactions : MempoolTransactionExtended [ ] ,
deletedTransactions : MempoolTransactionExtended [ ] ) = > Promise < void > ) | undefined ;
2019-07-21 17:59:47 +03:00
private txPerSecondArray : number [ ] = [ ] ;
private txPerSecond : number = 0 ;
2020-06-10 23:52:14 +07:00
private vBytesPerSecondArray : VbytesPerSecond [ ] = [ ] ;
2019-07-21 17:59:47 +03:00
private vBytesPerSecond : number = 0 ;
2020-06-18 13:54:54 +07:00
private mempoolProtection = 0 ;
2020-09-26 02:11:30 +07:00
private latestTransactions : any [ ] = [ ] ;
2019-07-21 17:59:47 +03:00
2023-03-09 17:45:08 +09:00
private ESPLORA_MISSING_TX_WARNING_THRESHOLD = 100 ;
private SAMPLE_TIME = 10000 ; // In ms
private timer = new Date ( ) . getTime ( ) ;
private missingTxCount = 0 ;
2023-04-27 10:19:12 +09:00
private mainLoopTimeout : number = 120000 ;
2019-07-21 17:59:47 +03:00
constructor ( ) {
setInterval ( this . updateTxPerSecond . bind ( this ) , 1000 ) ;
}
2022-02-11 21:25:58 +09:00
/ * *
* Return true if we should leave resources available for mempool tx caching
* /
public hasPriority ( ) : boolean {
if ( this . inSync ) {
return false ;
} else {
return this . mempoolCacheDelta == - 1 || this . mempoolCacheDelta > 25 ;
}
}
2021-01-20 17:16:43 +07:00
public isInSync ( ) : boolean {
2020-04-01 20:06:44 +07:00
return this . inSync ;
}
2021-01-20 17:16:43 +07:00
public setOutOfSync ( ) : void {
this . inSync = false ;
loadingIndicators . setProgress ( 'mempool' , 99 ) ;
}
2020-09-26 02:11:30 +07:00
public getLatestTransactions() {
return this . latestTransactions ;
}
2023-05-29 15:56:29 -04:00
public setMempoolChangedCallback ( fn : ( newMempool : { [ txId : string ] : MempoolTransactionExtended ; } ,
newTransactions : MempoolTransactionExtended [ ] , deletedTransactions : MempoolTransactionExtended [ ] ) = > void ) : void {
2019-07-21 17:59:47 +03:00
this . mempoolChangedCallback = fn ;
}
2023-05-29 15:56:29 -04:00
public setAsyncMempoolChangedCallback ( fn : ( newMempool : { [ txId : string ] : MempoolTransactionExtended ; } ,
newTransactions : MempoolTransactionExtended [ ] , deletedTransactions : MempoolTransactionExtended [ ] ) = > Promise < void > ) : void {
2023-04-30 15:28:34 -06:00
this . $asyncMempoolChangedCallback = fn ;
2022-11-16 18:18:59 -06:00
}
2023-05-29 15:56:29 -04:00
public getMempool ( ) : { [ txid : string ] : MempoolTransactionExtended } {
2020-02-16 22:15:07 +07:00
return this . mempoolCache ;
2019-07-21 17:59:47 +03:00
}
2023-05-29 15:56:29 -04:00
public getSpendMap ( ) : Map < string , MempoolTransactionExtended > {
2023-05-18 09:51:41 -04:00
return this . spendMap ;
}
2023-05-29 15:56:29 -04:00
public async $setMempool ( mempoolData : { [ txId : string ] : MempoolTransactionExtended } ) {
2020-02-16 22:15:07 +07:00
this . mempoolCache = mempoolData ;
2023-05-29 15:56:29 -04:00
for ( const txid of Object . keys ( this . mempoolCache ) ) {
if ( this . mempoolCache [ txid ] . sigops == null || this . mempoolCache [ txid ] . effectiveFeePerVsize == null ) {
this . mempoolCache [ txid ] = transactionUtils . extendMempoolTransaction ( this . mempoolCache [ txid ] ) ;
}
}
2020-07-03 23:45:19 +07:00
if ( this . mempoolChangedCallback ) {
this . mempoolChangedCallback ( this . mempoolCache , [ ] , [ ] ) ;
}
2023-04-30 15:28:34 -06:00
if ( this . $asyncMempoolChangedCallback ) {
await this . $asyncMempoolChangedCallback ( this . mempoolCache , [ ] , [ ] ) ;
2022-11-16 18:18:59 -06:00
}
2023-05-18 09:51:41 -04:00
this . addToSpendMap ( Object . values ( this . mempoolCache ) ) ;
2019-07-21 17:59:47 +03:00
}
2020-10-18 21:47:47 +07:00
public async $updateMemPoolInfo() {
2021-09-15 01:47:24 +04:00
this . mempoolInfo = await this . $getMempoolInfo ( ) ;
2020-02-17 20:39:20 +07:00
}
2021-02-24 12:26:55 +07:00
public getMempoolInfo ( ) : IBitcoinApi . MempoolInfo {
2019-07-21 17:59:47 +03:00
return this . mempoolInfo ;
}
public getTxPerSecond ( ) : number {
return this . txPerSecond ;
}
public getVBytesPerSecond ( ) : number {
return this . vBytesPerSecond ;
}
2020-02-28 01:09:07 +07:00
public getFirstSeenForTransactions ( txIds : string [ ] ) : number [ ] {
const txTimes : number [ ] = [ ] ;
txIds . forEach ( ( txId : string ) = > {
2020-12-28 04:47:22 +07:00
const tx = this . mempoolCache [ txId ] ;
if ( tx && tx . firstSeen ) {
txTimes . push ( tx . firstSeen ) ;
2020-02-28 01:09:07 +07:00
} else {
txTimes . push ( 0 ) ;
}
} ) ;
return txTimes ;
}
2023-04-07 09:41:25 +09:00
public async $updateMempool ( transactions : string [ ] ) : Promise < void > {
2022-08-19 17:54:52 +04:00
logger . debug ( ` Updating mempool... ` ) ;
2023-04-27 10:19:12 +09:00
// warn if this run stalls the main loop for more than 2 minutes
const timer = this . startTimer ( ) ;
2019-07-21 17:59:47 +03:00
const start = new Date ( ) . getTime ( ) ;
let hasChange : boolean = false ;
2020-06-18 13:54:54 +07:00
const currentMempoolSize = Object . keys ( this . mempoolCache ) . length ;
2023-04-27 10:19:12 +09:00
this . updateTimerProgress ( timer , 'got raw mempool' ) ;
2020-10-18 21:47:47 +07:00
const diff = transactions . length - currentMempoolSize ;
2023-05-29 15:56:29 -04:00
const newTransactions : MempoolTransactionExtended [ ] = [ ] ;
2020-10-18 21:47:47 +07:00
2022-02-11 21:25:58 +09:00
this . mempoolCacheDelta = Math . abs ( diff ) ;
2021-01-05 18:57:06 +07:00
if ( ! this . inSync ) {
2023-05-07 18:47:56 +04:00
loadingIndicators . setProgress ( 'mempool' , currentMempoolSize / transactions . length * 100 ) ;
2021-01-05 18:57:06 +07:00
}
2023-03-09 17:45:08 +09:00
// https://github.com/mempool/mempool/issues/3283
const logEsplora404 = ( missingTxCount , threshold , time ) = > {
const log = ` In the past ${ time / 1000 } seconds, esplora tx API replied ${ missingTxCount } times with a 404 error code while updating nodejs backend mempool ` ;
if ( missingTxCount >= threshold ) {
logger . warn ( log ) ;
} else if ( missingTxCount > 0 ) {
logger . debug ( log ) ;
}
} ;
2023-05-07 18:47:56 +04:00
let loggerTimer = new Date ( ) . getTime ( ) / 1000 ;
2020-10-18 21:47:47 +07:00
for ( const txid of transactions ) {
if ( ! this . mempoolCache [ txid ] ) {
2021-01-24 02:51:22 +07:00
try {
2023-05-29 15:56:29 -04:00
const transaction = await transactionUtils . $getMempoolTransactionExtended ( txid , false , false , false ) ;
2023-04-27 10:19:12 +09:00
this . updateTimerProgress ( timer , 'fetched new transaction' ) ;
2020-10-18 21:47:47 +07:00
this . mempoolCache [ txid ] = transaction ;
if ( this . inSync ) {
this . txPerSecondArray . push ( new Date ( ) . getTime ( ) ) ;
this . vBytesPerSecondArray . push ( {
unixTime : new Date ( ) . getTime ( ) ,
vSize : transaction.vsize ,
} ) ;
}
hasChange = true ;
newTransactions . push ( transaction ) ;
2023-03-09 17:45:08 +09:00
} catch ( e : any ) {
if ( config . MEMPOOL . BACKEND === 'esplora' && e . response ? . status === 404 ) {
this . missingTxCount ++ ;
}
2022-08-19 17:54:52 +04:00
logger . debug ( ` Error finding transaction ' ${ txid } ' in the mempool: ` + ( e instanceof Error ? e.message : e ) ) ;
2019-11-15 17:27:12 +08:00
}
2019-07-21 17:59:47 +03:00
}
2023-05-07 18:47:56 +04:00
const elapsedSeconds = Math . round ( ( new Date ( ) . getTime ( ) / 1000 ) - loggerTimer ) ;
if ( elapsedSeconds > 4 ) {
const progress = ( currentMempoolSize + newTransactions . length ) / transactions . length * 100 ;
logger . debug ( ` Mempool is synchronizing. Processed ${ newTransactions . length } / ${ diff } txs ( ${ Math . round ( progress ) } %) ` ) ;
loadingIndicators . setProgress ( 'mempool' , progress ) ;
loggerTimer = new Date ( ) . getTime ( ) / 1000 ;
2020-06-18 13:54:54 +07:00
}
2020-10-18 21:47:47 +07:00
}
2020-06-09 02:32:24 +07:00
2023-03-09 17:45:08 +09:00
// Reset esplora 404 counter and log a warning if needed
const elapsedTime = new Date ( ) . getTime ( ) - this . timer ;
if ( elapsedTime > this . SAMPLE_TIME ) {
logEsplora404 ( this . missingTxCount , this . ESPLORA_MISSING_TX_WARNING_THRESHOLD , elapsedTime ) ;
this . timer = new Date ( ) . getTime ( ) ;
this . missingTxCount = 0 ;
}
2020-10-18 21:47:47 +07:00
// Prevent mempool from clear on bitcoind restart by delaying the deletion
2020-10-19 17:30:47 +07:00
if ( this . mempoolProtection === 0
&& currentMempoolSize > 20000
&& transactions . length / currentMempoolSize <= 0.80
) {
2020-10-18 21:47:47 +07:00
this . mempoolProtection = 1 ;
this . inSync = false ;
logger . warn ( ` Mempool clear protection triggered because transactions.length: ${ transactions . length } and currentMempoolSize: ${ currentMempoolSize } . ` ) ;
setTimeout ( ( ) = > {
this . mempoolProtection = 2 ;
logger . warn ( 'Mempool clear protection resumed.' ) ;
2021-02-14 20:32:00 +07:00
} , 1000 * 60 * config . MEMPOOL . CLEAR_PROTECTION_MINUTES ) ;
2020-10-18 21:47:47 +07:00
}
2020-06-18 13:54:54 +07:00
2023-05-29 15:56:29 -04:00
const deletedTransactions : MempoolTransactionExtended [ ] = [ ] ;
2020-10-18 21:47:47 +07:00
if ( this . mempoolProtection !== 1 ) {
this . mempoolProtection = 0 ;
// Index object for faster search
const transactionsObject = { } ;
transactions . forEach ( ( txId ) = > transactionsObject [ txId ] = true ) ;
2023-04-07 09:41:25 +09:00
// Delete evicted transactions from mempool
2020-10-18 21:47:47 +07:00
for ( const tx in this . mempoolCache ) {
2023-04-07 09:41:25 +09:00
if ( ! transactionsObject [ tx ] ) {
2020-10-18 21:47:47 +07:00
deletedTransactions . push ( this . mempoolCache [ tx ] ) ;
2019-07-21 17:59:47 +03:00
}
2020-06-08 18:55:53 +07:00
}
2023-04-07 09:41:25 +09:00
for ( const tx of deletedTransactions ) {
delete this . mempoolCache [ tx . txid ] ;
}
2020-10-18 21:47:47 +07:00
}
2019-07-21 17:59:47 +03:00
2020-10-18 21:47:47 +07:00
const newTransactionsStripped = newTransactions . map ( ( tx ) = > Common . stripTransaction ( tx ) ) ;
this . latestTransactions = newTransactionsStripped . concat ( this . latestTransactions ) . slice ( 0 , 6 ) ;
2020-09-26 02:11:30 +07:00
2022-02-11 20:35:53 +09:00
if ( ! this . inSync && transactions . length === Object . keys ( this . mempoolCache ) . length ) {
2020-10-18 21:47:47 +07:00
this . inSync = true ;
2021-04-13 14:03:36 +09:00
logger . notice ( 'The mempool is now in sync!' ) ;
2021-01-05 18:57:06 +07:00
loadingIndicators . setProgress ( 'mempool' , 100 ) ;
2020-10-18 21:47:47 +07:00
}
2019-07-21 17:59:47 +03:00
2022-02-11 21:25:58 +09:00
this . mempoolCacheDelta = Math . abs ( transactions . length - Object . keys ( this . mempoolCache ) . length ) ;
2020-10-18 21:47:47 +07:00
if ( this . mempoolChangedCallback && ( hasChange || deletedTransactions . length ) ) {
this . mempoolChangedCallback ( this . mempoolCache , newTransactions , deletedTransactions ) ;
2019-07-21 17:59:47 +03:00
}
2023-04-30 15:28:34 -06:00
if ( this . $asyncMempoolChangedCallback && ( hasChange || deletedTransactions . length ) ) {
2023-04-27 10:19:12 +09:00
this . updateTimerProgress ( timer , 'running async mempool callback' ) ;
2023-04-30 15:28:34 -06:00
await this . $asyncMempoolChangedCallback ( this . mempoolCache , newTransactions , deletedTransactions ) ;
2023-04-27 10:19:12 +09:00
this . updateTimerProgress ( timer , 'completed async mempool callback' ) ;
2022-11-16 18:18:59 -06:00
}
2020-10-18 21:47:47 +07:00
const end = new Date ( ) . getTime ( ) ;
const time = end - start ;
2022-08-19 17:54:52 +04:00
logger . debug ( ` Mempool updated in ${ time / 1000 } seconds. New size: ${ Object . keys ( this . mempoolCache ) . length } ( ${ diff > 0 ? '+' + diff : diff } ) ` ) ;
2023-04-27 10:19:12 +09:00
this . clearTimer ( timer ) ;
}
private startTimer() {
const state : any = {
start : Date.now ( ) ,
progress : 'begin $updateMempool' ,
timer : null ,
} ;
state . timer = setTimeout ( ( ) = > {
2023-05-01 00:16:23 +04:00
logger . err ( ` $ updateMempool stalled at " ${ state . progress } " ` ) ;
2023-04-27 10:19:12 +09:00
} , this . mainLoopTimeout ) ;
return state ;
}
private updateTimerProgress ( state , msg ) {
state . progress = msg ;
}
private clearTimer ( state ) {
if ( state . timer ) {
clearTimeout ( state . timer ) ;
}
2019-07-21 17:59:47 +03:00
}
2023-05-29 15:56:29 -04:00
public handleRbfTransactions ( rbfTransactions : { [ txid : string ] : MempoolTransactionExtended [ ] ; } ) : void {
2022-03-08 14:49:25 +01:00
for ( const rbfTransaction in rbfTransactions ) {
2022-12-17 09:39:06 -06:00
if ( this . mempoolCache [ rbfTransaction ] && rbfTransactions [ rbfTransaction ] ? . length ) {
2022-03-08 14:49:25 +01:00
// Store replaced transactions
2022-12-17 09:39:06 -06:00
rbfCache . add ( rbfTransactions [ rbfTransaction ] , this . mempoolCache [ rbfTransaction ] ) ;
2022-03-08 14:49:25 +01:00
}
}
}
2023-05-31 11:37:13 -04:00
public handleMinedRbfTransactions ( rbfTransactions : { [ txid : string ] : { replaced : MempoolTransactionExtended [ ] , replacedBy : TransactionExtended } } ) : void {
2023-05-18 09:51:41 -04:00
for ( const rbfTransaction in rbfTransactions ) {
if ( rbfTransactions [ rbfTransaction ] . replacedBy && rbfTransactions [ rbfTransaction ] ? . replaced ? . length ) {
// Store replaced transactions
2023-05-31 11:37:13 -04:00
rbfCache . add ( rbfTransactions [ rbfTransaction ] . replaced , transactionUtils . extendMempoolTransaction ( rbfTransactions [ rbfTransaction ] . replacedBy ) ) ;
2023-05-18 09:51:41 -04:00
}
}
}
2023-05-29 15:56:29 -04:00
public addToSpendMap ( transactions : MempoolTransactionExtended [ ] ) : void {
2023-05-18 09:51:41 -04:00
for ( const tx of transactions ) {
for ( const vin of tx . vin ) {
this . spendMap . set ( ` ${ vin . txid } : ${ vin . vout } ` , tx ) ;
}
}
}
2023-05-31 11:37:13 -04:00
public removeFromSpendMap ( transactions : TransactionExtended [ ] ) : void {
2023-05-18 09:51:41 -04:00
for ( const tx of transactions ) {
for ( const vin of tx . vin ) {
const key = ` ${ vin . txid } : ${ vin . vout } ` ;
if ( this . spendMap . get ( key ) ? . txid === tx . txid ) {
this . spendMap . delete ( key ) ;
}
}
}
}
2019-07-21 17:59:47 +03:00
private updateTxPerSecond() {
2020-10-19 11:57:02 +07:00
const nowMinusTimeSpan = new Date ( ) . getTime ( ) - ( 1000 * config . STATISTICS . TX_PER_SECOND_SAMPLE_PERIOD ) ;
2019-07-21 17:59:47 +03:00
this . txPerSecondArray = this . txPerSecondArray . filter ( ( unixTime ) = > unixTime > nowMinusTimeSpan ) ;
2020-10-19 11:57:02 +07:00
this . txPerSecond = this . txPerSecondArray . length / config . STATISTICS . TX_PER_SECOND_SAMPLE_PERIOD || 0 ;
2019-07-21 17:59:47 +03:00
this . vBytesPerSecondArray = this . vBytesPerSecondArray . filter ( ( data ) = > data . unixTime > nowMinusTimeSpan ) ;
if ( this . vBytesPerSecondArray . length ) {
this . vBytesPerSecond = Math . round (
2020-10-19 11:57:02 +07:00
this . vBytesPerSecondArray . map ( ( data ) = > data . vSize ) . reduce ( ( a , b ) = > a + b ) / config . STATISTICS . TX_PER_SECOND_SAMPLE_PERIOD
2019-07-21 17:59:47 +03:00
) ;
}
}
2021-03-21 06:06:03 +07:00
2021-09-15 01:47:24 +04:00
private $getMempoolInfo() {
2021-09-19 02:40:16 +04:00
if ( config . MEMPOOL . USE_SECOND_NODE_FOR_MINFEE ) {
2021-09-15 01:47:24 +04:00
return Promise . all ( [
bitcoinClient . getMempoolInfo ( ) ,
bitcoinSecondClient . getMempoolInfo ( )
] ) . then ( ( [ mempoolInfo , secondMempoolInfo ] ) = > {
mempoolInfo . maxmempool = secondMempoolInfo . maxmempool ;
mempoolInfo . mempoolminfee = secondMempoolInfo . mempoolminfee ;
mempoolInfo . minrelaytxfee = secondMempoolInfo . minrelaytxfee ;
return mempoolInfo ;
} ) ;
}
return bitcoinClient . getMempoolInfo ( ) ;
}
2019-07-21 17:59:47 +03:00
}
export default new Mempool ( ) ;