2020-12-20 22:36:36 +07:00
import config from '../config' ;
import bitcoinApi from './bitcoin/bitcoin-api-factory' ;
2020-10-13 15:27:52 +07:00
import logger from '../logger' ;
2020-02-23 19:16:50 +07:00
import memPool from './mempool' ;
2022-01-05 15:41:14 +09:00
import { BlockExtended , PoolTag , TransactionExtended , TransactionMinerInfo } from '../mempool.interfaces' ;
2020-05-24 16:29:30 +07:00
import { Common } from './common' ;
2020-10-27 00:05:06 +07:00
import diskCache from './disk-cache' ;
2020-12-21 23:08:34 +07:00
import transactionUtils from './transaction-utils' ;
2021-09-15 01:47:24 +04:00
import bitcoinClient from './bitcoin/bitcoin-client' ;
2022-01-05 15:41:14 +09:00
import { IEsploraApi } from './bitcoin/esplora-api.interface' ;
import poolsRepository from '../repositories/PoolsRepository' ;
import blocksRepository from '../repositories/BlocksRepository' ;
2022-01-18 17:37:04 +09:00
import BitcoinApi from './bitcoin/bitcoin-api' ;
2019-07-21 17:59:47 +03:00
class Blocks {
2020-12-28 04:47:22 +07:00
private blocks : BlockExtended [ ] = [ ] ;
2019-08-29 01:17:31 +02:00
private currentBlockHeight = 0 ;
2021-07-23 14:35:04 +03:00
private currentDifficulty = 0 ;
2020-09-21 19:41:12 +07:00
private lastDifficultyAdjustmentTime = 0 ;
2021-07-23 14:35:04 +03:00
private previousDifficultyRetarget = 0 ;
2020-12-28 04:47:22 +07:00
private newBlockCallbacks : ( ( block : BlockExtended , txIds : string [ ] , transactions : TransactionExtended [ ] ) = > void ) [ ] = [ ] ;
2019-08-29 01:17:31 +02:00
2020-02-16 22:15:07 +07:00
constructor ( ) { }
2019-07-21 17:59:47 +03:00
2020-12-28 04:47:22 +07:00
public getBlocks ( ) : BlockExtended [ ] {
2019-07-21 17:59:47 +03:00
return this . blocks ;
}
2020-12-28 04:47:22 +07:00
public setBlocks ( blocks : BlockExtended [ ] ) {
2020-02-29 21:52:04 +07:00
this . blocks = blocks ;
}
2020-12-28 04:47:22 +07:00
public setNewBlockCallback ( fn : ( block : BlockExtended , txIds : string [ ] , transactions : TransactionExtended [ ] ) = > void ) {
2020-09-27 17:21:18 +07:00
this . newBlockCallbacks . push ( fn ) ;
2019-07-21 17:59:47 +03:00
}
2022-01-05 15:41:14 +09:00
/ * *
* Return the list of transaction for a block
* @param blockHash
* @param blockHeight
* @param onlyCoinbase - Set to true if you only need the coinbase transaction
* @returns Promise < TransactionExtended [ ] >
* /
2022-01-20 17:20:02 +09:00
private async $getTransactionsExtended ( blockHash : string , blockHeight : number , onlyCoinbase : boolean ) : Promise < TransactionExtended [ ] > {
2022-01-05 15:41:14 +09:00
const transactions : TransactionExtended [ ] = [ ] ;
const txIds : string [ ] = await bitcoinApi . $getTxIdsForBlock ( blockHash ) ;
const mempool = memPool . getMempool ( ) ;
let transactionsFound = 0 ;
let transactionsFetched = 0 ;
for ( let i = 0 ; i < txIds . length ; i ++ ) {
if ( mempool [ txIds [ i ] ] ) {
// We update blocks before the mempool (index.ts), therefore we can
// optimize here by directly fetching txs in the "outdated" mempool
transactions . push ( mempool [ txIds [ i ] ] ) ;
transactionsFound ++ ;
} else if ( config . MEMPOOL . BACKEND === 'esplora' || memPool . isInSync ( ) || i === 0 ) {
// Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...)
if ( i % ( Math . round ( ( txIds . length ) / 10 ) ) === 0 || i + 1 === txIds . length ) { // Avoid log spam
logger . debug ( ` Indexing tx ${ i + 1 } of ${ txIds . length } in block # ${ blockHeight } ` ) ;
}
try {
const tx = await transactionUtils . $getTransactionExtended ( txIds [ i ] ) ;
transactions . push ( tx ) ;
transactionsFetched ++ ;
} catch ( e ) {
logger . debug ( 'Error fetching block tx: ' + ( e instanceof Error ? e.message : e ) ) ;
if ( i === 0 ) {
throw new Error ( 'Failed to fetch Coinbase transaction: ' + txIds [ i ] ) ;
}
}
}
if ( onlyCoinbase === true ) {
break ; // Fetch the first transaction and exit
}
}
transactions . forEach ( ( tx ) = > {
if ( ! tx . cpfpChecked ) {
Common . setRelativesAndGetCpfpInfo ( tx , mempool ) ; // Child Pay For Parent
}
} ) ;
logger . debug ( ` ${ transactionsFound } of ${ txIds . length } found in mempool. ${ transactionsFetched } fetched through backend service. ` ) ;
return transactions ;
}
/ * *
* Return a block with additional data ( reward , coinbase , fees . . . )
* @param block
* @param transactions
* @returns BlockExtended
* /
2022-01-20 17:20:02 +09:00
private getBlockExtended ( block : IEsploraApi.Block , transactions : TransactionExtended [ ] ) : BlockExtended {
2022-01-05 15:41:14 +09:00
const blockExtended : BlockExtended = Object . assign ( { } , block ) ;
blockExtended . reward = transactions [ 0 ] . vout . reduce ( ( acc , curr ) = > acc + curr . value , 0 ) ;
blockExtended . coinbaseTx = transactionUtils . stripCoinbaseTransaction ( transactions [ 0 ] ) ;
const transactionsTmp = [ . . . transactions ] ;
transactionsTmp . shift ( ) ;
transactionsTmp . sort ( ( a , b ) = > b . effectiveFeePerVsize - a . effectiveFeePerVsize ) ;
blockExtended . medianFee = transactionsTmp . length > 0 ? Common . median ( transactionsTmp . map ( ( tx ) = > tx . effectiveFeePerVsize ) ) : 0 ;
blockExtended . feeRange = transactionsTmp . length > 0 ? Common . getFeesInRange ( transactionsTmp , 8 ) : [ 0 , 0 ] ;
return blockExtended ;
}
/ * *
* Try to find which miner found the block
* @param txMinerInfo
* @returns
* /
2022-01-20 17:20:02 +09:00
private async $findBlockMiner ( txMinerInfo : TransactionMinerInfo | undefined ) : Promise < PoolTag > {
2022-01-05 15:41:14 +09:00
if ( txMinerInfo === undefined ) {
2022-01-06 19:59:33 +09:00
return await poolsRepository . $getUnknownPool ( ) ;
2022-01-05 15:41:14 +09:00
}
const asciiScriptSig = transactionUtils . hex2ascii ( txMinerInfo . vin [ 0 ] . scriptsig ) ;
const address = txMinerInfo . vout [ 0 ] . scriptpubkey_address ;
const pools : PoolTag [ ] = await poolsRepository . $getPools ( ) ;
for ( let i = 0 ; i < pools . length ; ++ i ) {
if ( address !== undefined ) {
2022-01-20 17:20:02 +09:00
const addresses : string [ ] = JSON . parse ( pools [ i ] . addresses ) ;
2022-01-05 15:41:14 +09:00
if ( addresses . indexOf ( address ) !== - 1 ) {
return pools [ i ] ;
}
}
2022-01-20 17:20:02 +09:00
const regexes : string [ ] = JSON . parse ( pools [ i ] . regexes ) ;
2022-01-05 15:41:14 +09:00
for ( let y = 0 ; y < regexes . length ; ++ y ) {
2022-01-20 17:20:02 +09:00
const match = asciiScriptSig . match ( regexes [ y ] ) ;
2022-01-05 15:41:14 +09:00
if ( match !== null ) {
return pools [ i ] ;
}
}
}
2022-01-06 19:59:33 +09:00
return await poolsRepository . $getUnknownPool ( ) ;
2022-01-05 15:41:14 +09:00
}
/ * *
* Index all blocks metadata for the mining dashboard
* /
public async $generateBlockDatabase() {
2022-01-21 00:08:51 +09:00
if ( [ 'mainnet' , 'testnet' , 'signet' ] . includes ( config . MEMPOOL . NETWORK ) === false ) {
return ;
}
2022-01-18 17:37:04 +09:00
let currentBlockHeight = await bitcoinClient . getBlockCount ( ) ;
const indexedBlockCount = await blocksRepository . $blockCount ( ) ;
logger . info ( ` Starting block indexing. Current tip at block # ${ currentBlockHeight } ` ) ;
logger . info ( ` Need to index ${ currentBlockHeight - indexedBlockCount } blocks. Working on it! ` ) ;
const chunkSize = 10000 ;
while ( currentBlockHeight >= 0 ) {
const endBlock = Math . max ( 0 , currentBlockHeight - chunkSize + 1 ) ;
const missingBlockHeights : number [ ] = await blocksRepository . $getMissingBlocksBetweenHeights (
currentBlockHeight , endBlock ) ;
if ( missingBlockHeights . length <= 0 ) {
logger . debug ( ` No missing blocks between # ${ currentBlockHeight } to # ${ endBlock } , moving on ` ) ;
currentBlockHeight -= chunkSize ;
2022-01-05 15:41:14 +09:00
continue ;
}
2022-01-18 17:37:04 +09:00
logger . info ( ` Indexing ${ chunkSize } blocks from # ${ currentBlockHeight } to # ${ endBlock } ` ) ;
for ( const blockHeight of missingBlockHeights ) {
try {
logger . debug ( ` Indexing block # ${ blockHeight } ` ) ;
const blockHash = await bitcoinApi . $getBlockHash ( blockHeight ) ;
const block = await bitcoinApi . $getBlock ( blockHash ) ;
const transactions = await this . $getTransactionsExtended ( blockHash , block . height , true ) ;
const blockExtended = this . getBlockExtended ( block , transactions ) ;
const miner = await this . $findBlockMiner ( blockExtended . coinbaseTx ) ;
const coinbase : IEsploraApi.Transaction = await bitcoinApi . $getRawTransaction ( transactions [ 0 ] . txid , true ) ;
await blocksRepository . $saveBlockInDatabase ( blockExtended , blockHash , coinbase . hex , miner ) ;
} catch ( e ) {
logger . err ( ` Something went wrong while indexing blocks. ` + e ) ;
}
}
currentBlockHeight -= chunkSize ;
2022-01-05 15:41:14 +09:00
}
}
2020-10-18 21:47:47 +07:00
public async $updateBlocks() {
2020-12-21 23:08:34 +07:00
const blockHeightTip = await bitcoinApi . $getBlockHeightTip ( ) ;
2019-07-21 17:59:47 +03:00
2020-10-18 21:47:47 +07:00
if ( this . blocks . length === 0 ) {
2021-07-31 17:56:10 +03:00
this . currentBlockHeight = blockHeightTip - config . MEMPOOL . INITIAL_BLOCKS_AMOUNT ;
2020-10-18 21:47:47 +07:00
} else {
this . currentBlockHeight = this . blocks [ this . blocks . length - 1 ] . height ;
}
2021-07-31 17:56:10 +03:00
if ( blockHeightTip - this . currentBlockHeight > config . MEMPOOL . INITIAL_BLOCKS_AMOUNT * 2 ) {
logger . info ( ` ${ blockHeightTip - this . currentBlockHeight } blocks since tip. Fast forwarding to the ${ config . MEMPOOL . INITIAL_BLOCKS_AMOUNT } recent blocks ` ) ;
this . currentBlockHeight = blockHeightTip - config . MEMPOOL . INITIAL_BLOCKS_AMOUNT ;
2020-10-18 21:47:47 +07:00
}
if ( ! this . lastDifficultyAdjustmentTime ) {
2021-09-15 01:47:24 +04:00
const blockchainInfo = await bitcoinClient . getBlockchainInfo ( ) ;
2021-08-01 15:49:26 +03:00
if ( blockchainInfo . blocks === blockchainInfo . headers ) {
const heightDiff = blockHeightTip % 2016 ;
const blockHash = await bitcoinApi . $getBlockHash ( blockHeightTip - heightDiff ) ;
const block = await bitcoinApi . $getBlock ( blockHash ) ;
this . lastDifficultyAdjustmentTime = block . timestamp ;
this . currentDifficulty = block . difficulty ;
const previousPeriodBlockHash = await bitcoinApi . $getBlockHash ( blockHeightTip - heightDiff - 2016 ) ;
const previousPeriodBlock = await bitcoinApi . $getBlock ( previousPeriodBlockHash ) ;
this . previousDifficultyRetarget = ( block . difficulty - previousPeriodBlock . difficulty ) / previousPeriodBlock . difficulty * 100 ;
logger . debug ( ` Initial difficulty adjustment data set. ` ) ;
} else {
logger . debug ( ` Blockchain headers ( ${ blockchainInfo . headers } ) and blocks ( ${ blockchainInfo . blocks } ) not in sync. Waiting... ` ) ;
}
2020-10-18 21:47:47 +07:00
}
while ( this . currentBlockHeight < blockHeightTip ) {
if ( this . currentBlockHeight === 0 ) {
this . currentBlockHeight = blockHeightTip ;
2019-07-21 17:59:47 +03:00
} else {
2020-10-18 21:47:47 +07:00
this . currentBlockHeight ++ ;
logger . debug ( ` New block found (# ${ this . currentBlockHeight } )! ` ) ;
2019-07-21 17:59:47 +03:00
}
2020-12-21 23:08:34 +07:00
const blockHash = await bitcoinApi . $getBlockHash ( this . currentBlockHeight ) ;
const block = await bitcoinApi . $getBlock ( blockHash ) ;
const txIds : string [ ] = await bitcoinApi . $getTxIdsForBlock ( blockHash ) ;
2022-01-05 15:41:14 +09:00
const transactions = await this . $getTransactionsExtended ( blockHash , block . height , false ) ;
const blockExtended : BlockExtended = this . getBlockExtended ( block , transactions ) ;
const coinbase : IEsploraApi.Transaction = await bitcoinApi . $getRawTransaction ( transactions [ 0 ] . txid , true ) ;
2022-01-21 00:08:51 +09:00
if ( [ 'mainnet' , 'testnet' , 'signet' ] . includes ( config . MEMPOOL . NETWORK ) === true ) {
2022-01-21 18:14:42 +09:00
const miner = await this . $findBlockMiner ( blockExtended . coinbaseTx ) ;
2022-01-21 00:08:51 +09:00
await blocksRepository . $saveBlockInDatabase ( blockExtended , blockHash , coinbase . hex , miner ) ;
}
2020-09-21 19:41:12 +07:00
2020-10-18 21:47:47 +07:00
if ( block . height % 2016 === 0 ) {
2021-07-23 14:35:04 +03:00
this . previousDifficultyRetarget = ( block . difficulty - this . currentDifficulty ) / this . currentDifficulty * 100 ;
2020-10-18 21:47:47 +07:00
this . lastDifficultyAdjustmentTime = block . timestamp ;
2021-07-23 14:35:04 +03:00
this . currentDifficulty = block . difficulty ;
2020-10-18 21:47:47 +07:00
}
2019-07-21 17:59:47 +03:00
2020-12-28 04:47:22 +07:00
this . blocks . push ( blockExtended ) ;
2021-07-31 17:56:10 +03:00
if ( this . blocks . length > config . MEMPOOL . INITIAL_BLOCKS_AMOUNT * 4 ) {
this . blocks = this . blocks . slice ( - config . MEMPOOL . INITIAL_BLOCKS_AMOUNT * 4 ) ;
2019-07-21 17:59:47 +03:00
}
2020-10-18 21:47:47 +07:00
if ( this . newBlockCallbacks . length ) {
2020-12-28 04:47:22 +07:00
this . newBlockCallbacks . forEach ( ( cb ) = > cb ( blockExtended , txIds , transactions ) ) ;
2020-10-18 21:47:47 +07:00
}
2021-01-22 23:20:39 +07:00
if ( memPool . isInSync ( ) ) {
diskCache . $saveCacheToDisk ( ) ;
}
2022-01-05 15:41:14 +09:00
return ;
2019-08-29 01:17:31 +02:00
}
}
2020-05-24 22:45:45 +07:00
2020-09-21 19:41:12 +07:00
public getLastDifficultyAdjustmentTime ( ) : number {
return this . lastDifficultyAdjustmentTime ;
}
2021-07-23 14:35:04 +03:00
public getPreviousDifficultyRetarget ( ) : number {
return this . previousDifficultyRetarget ;
}
2020-12-21 23:08:34 +07:00
public getCurrentBlockHeight ( ) : number {
return this . currentBlockHeight ;
}
2019-07-21 17:59:47 +03:00
}
export default new Blocks ( ) ;