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-02-08 15:47:43 +09:00
import loadingIndicators from './loading-indicators' ;
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 ) [ ] = [ ] ;
2022-01-24 19:57:54 +09:00
private blockIndexingStarted = false ;
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-02-10 23:02:12 +09:00
private async $getTransactionsExtended (
blockHash : string ,
blockHeight : number ,
onlyCoinbase : boolean ,
quiet : boolean = false ,
) : 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 ++ ;
2022-02-11 21:25:58 +09:00
} else if ( config . MEMPOOL . BACKEND === 'esplora' || ! memPool . hasPriority ( ) || i === 0 ) {
2022-01-05 15:41:14 +09:00
// Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...)
2022-02-10 23:02:12 +09:00
if ( ! quiet && ( i % ( Math . round ( ( txIds . length ) / 10 ) ) === 0 || i + 1 === txIds . length ) ) { // Avoid log spam
2022-01-05 15:41:14 +09:00
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
}
} ) ;
2022-02-10 23:02:12 +09:00
if ( ! quiet ) {
logger . debug ( ` ${ transactionsFound } of ${ txIds . length } found in mempool. ${ transactionsFetched } fetched through backend service. ` ) ;
}
2022-01-05 15:41:14 +09:00
return transactions ;
}
/ * *
* Return a block with additional data ( reward , coinbase , fees . . . )
* @param block
* @param transactions
* @returns BlockExtended
* /
2022-02-08 15:47:43 +09:00
private async $getBlockExtended ( block : IEsploraApi.Block , transactions : TransactionExtended [ ] ) : Promise < BlockExtended > {
const blockExtended : BlockExtended = Object . assign ( { extras : { } } , block ) ;
blockExtended . extras . reward = transactions [ 0 ] . vout . reduce ( ( acc , curr ) = > acc + curr . value , 0 ) ;
blockExtended . extras . coinbaseTx = transactionUtils . stripCoinbaseTransaction ( transactions [ 0 ] ) ;
2022-01-05 15:41:14 +09:00
const transactionsTmp = [ . . . transactions ] ;
transactionsTmp . shift ( ) ;
transactionsTmp . sort ( ( a , b ) = > b . effectiveFeePerVsize - a . effectiveFeePerVsize ) ;
2022-02-04 12:51:45 +09:00
2022-02-04 19:28:00 +09:00
blockExtended . extras . medianFee = transactionsTmp . length > 0 ?
2022-02-04 12:51:45 +09:00
Common . median ( transactionsTmp . map ( ( tx ) = > tx . effectiveFeePerVsize ) ) : 0 ;
2022-02-04 19:28:00 +09:00
blockExtended . extras . feeRange = transactionsTmp . length > 0 ?
2022-02-04 12:51:45 +09:00
Common . getFeesInRange ( transactionsTmp , 8 ) : [ 0 , 0 ] ;
2022-01-05 15:41:14 +09:00
2022-02-08 15:47:43 +09:00
const indexingAvailable =
[ 'mainnet' , 'testnet' , 'signet' ] . includes ( config . MEMPOOL . NETWORK ) &&
config . DATABASE . ENABLED === true ;
if ( indexingAvailable ) {
let pool : PoolTag ;
if ( blockExtended . extras ? . coinbaseTx !== undefined ) {
pool = await this . $findBlockMiner ( blockExtended . extras ? . coinbaseTx ) ;
} else {
pool = await poolsRepository . $getUnknownPool ( ) ;
}
2022-02-10 10:25:14 +09:00
blockExtended . extras . pool = {
id : pool.id ,
name : pool.name
} ;
2022-02-08 15:47:43 +09:00
}
2022-01-05 15:41:14 +09:00
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-25 18:33:46 +09:00
if ( txMinerInfo === undefined || txMinerInfo . vout . length < 1 ) {
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-24 19:57:54 +09:00
if ( [ 'mainnet' , 'testnet' , 'signet' ] . includes ( config . MEMPOOL . NETWORK ) === false || // Bitcoin only
2022-02-08 15:47:43 +09:00
config . MEMPOOL . INDEXING_BLOCKS_AMOUNT === 0 || // Indexing of older blocks must be enabled
2022-02-11 21:25:58 +09:00
memPool . hasPriority ( ) || // We sync the mempool first
2022-02-08 15:47:43 +09:00
this . blockIndexingStarted === true || // Indexing must not already be in progress
config . DATABASE . ENABLED === false
2022-01-24 19:57:54 +09:00
) {
2022-01-21 00:08:51 +09:00
return ;
}
2022-01-24 19:57:54 +09:00
const blockchainInfo = await bitcoinClient . getBlockchainInfo ( ) ;
2022-02-08 15:47:43 +09:00
if ( blockchainInfo . blocks !== blockchainInfo . headers ) { // Wait for node to sync
2022-01-24 19:57:54 +09:00
return ;
}
this . blockIndexingStarted = true ;
2022-02-10 23:02:12 +09:00
const startedAt = new Date ( ) . getTime ( ) / 1000 ;
2022-01-24 19:57:54 +09:00
2022-01-24 15:36:30 +09:00
try {
2022-01-24 19:57:54 +09:00
let currentBlockHeight = blockchainInfo . blocks ;
2022-01-25 18:33:46 +09:00
let indexingBlockAmount = config . MEMPOOL . INDEXING_BLOCKS_AMOUNT ;
if ( indexingBlockAmount <= - 1 ) {
indexingBlockAmount = currentBlockHeight + 1 ;
}
const lastBlockToIndex = Math . max ( 0 , currentBlockHeight - indexingBlockAmount + 1 ) ;
2022-01-24 15:36:30 +09:00
2022-01-24 17:43:11 +09:00
logger . info ( ` Indexing blocks from # ${ currentBlockHeight } to # ${ lastBlockToIndex } ` ) ;
2022-01-24 15:36:30 +09:00
const chunkSize = 10000 ;
2022-02-10 23:02:12 +09:00
let totaIndexed = 0 ;
2022-01-24 17:43:11 +09:00
while ( currentBlockHeight >= lastBlockToIndex ) {
const endBlock = Math . max ( 0 , lastBlockToIndex , currentBlockHeight - chunkSize + 1 ) ;
2022-01-24 15:36:30 +09:00
const missingBlockHeights : number [ ] = await blocksRepository . $getMissingBlocksBetweenHeights (
currentBlockHeight , endBlock ) ;
if ( missingBlockHeights . length <= 0 ) {
2022-01-24 17:43:11 +09:00
logger . debug ( ` No missing blocks between # ${ currentBlockHeight } to # ${ endBlock } ` ) ;
2022-01-24 15:36:30 +09:00
currentBlockHeight -= chunkSize ;
continue ;
}
2022-01-18 17:37:04 +09:00
2022-01-24 17:43:11 +09:00
logger . debug ( ` Indexing ${ missingBlockHeights . length } blocks from # ${ currentBlockHeight } to # ${ endBlock } ` ) ;
2022-01-24 15:36:30 +09:00
for ( const blockHeight of missingBlockHeights ) {
2022-01-24 17:43:11 +09:00
if ( blockHeight < lastBlockToIndex ) {
break ;
}
2022-01-24 15:36:30 +09:00
try {
2022-02-10 23:02:12 +09:00
if ( totaIndexed % 100 === 0 || blockHeight === lastBlockToIndex ) {
const elapsedSeconds = Math . max ( 1 , Math . round ( ( new Date ( ) . getTime ( ) / 1000 ) - startedAt ) ) ;
const blockPerSeconds = Math . round ( totaIndexed / elapsedSeconds ) ;
logger . debug ( ` Indexing block # ${ blockHeight } | ~ ${ blockPerSeconds } blocks/sec | total: ${ totaIndexed } | elapsed: ${ elapsedSeconds } seconds ` ) ;
}
2022-01-24 15:36:30 +09:00
const blockHash = await bitcoinApi . $getBlockHash ( blockHeight ) ;
const block = await bitcoinApi . $getBlock ( blockHash ) ;
2022-02-10 23:02:12 +09:00
const transactions = await this . $getTransactionsExtended ( blockHash , block . height , true , true ) ;
2022-02-08 15:47:43 +09:00
const blockExtended = await this . $getBlockExtended ( block , transactions ) ;
await blocksRepository . $saveBlockInDatabase ( blockExtended ) ;
2022-02-10 23:02:12 +09:00
++ totaIndexed ;
2022-01-24 15:36:30 +09:00
} catch ( e ) {
logger . err ( ` Something went wrong while indexing blocks. ` + e ) ;
}
2022-01-18 17:37:04 +09:00
}
2022-01-24 15:36:30 +09:00
currentBlockHeight -= chunkSize ;
}
2022-01-24 17:43:11 +09:00
logger . info ( 'Block indexing completed' ) ;
2022-01-24 15:36:30 +09:00
} catch ( e ) {
logger . err ( 'An error occured in $generateBlockDatabase(). Skipping block indexing. ' + e ) ;
2022-01-24 17:43:11 +09:00
console . log ( e ) ;
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 ) ;
2022-02-08 15:47:43 +09:00
const blockExtended : BlockExtended = await this . $getBlockExtended ( block , transactions ) ;
const indexingAvailable =
[ 'mainnet' , 'testnet' , 'signet' ] . includes ( config . MEMPOOL . NETWORK ) &&
config . DATABASE . ENABLED === true ;
if ( indexingAvailable ) {
await blocksRepository . $saveBlockInDatabase ( blockExtended ) ;
2022-01-21 00:08:51 +09:00
}
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
}
2022-02-11 21:25:58 +09:00
if ( ! memPool . hasPriority ( ) ) {
2021-01-22 23:20:39 +07:00
diskCache . $saveCacheToDisk ( ) ;
}
2019-08-29 01:17:31 +02:00
}
}
2020-05-24 22:45:45 +07:00
2022-02-08 15:47:43 +09:00
/ * *
* Index a block if it ' s missing from the database . Returns the block after indexing
* /
public async $indexBlock ( height : number ) : Promise < BlockExtended > {
const dbBlock = await blocksRepository . $getBlockByHeight ( height ) ;
if ( dbBlock != null ) {
return this . prepareBlock ( dbBlock ) ;
}
const blockHash = await bitcoinApi . $getBlockHash ( height ) ;
const block = await bitcoinApi . $getBlock ( blockHash ) ;
const transactions = await this . $getTransactionsExtended ( blockHash , block . height , true ) ;
const blockExtended = await this . $getBlockExtended ( block , transactions ) ;
await blocksRepository . $saveBlockInDatabase ( blockExtended ) ;
return blockExtended ;
}
public async $getBlocksExtras ( fromHeight : number ) : Promise < BlockExtended [ ] > {
const indexingAvailable =
[ 'mainnet' , 'testnet' , 'signet' ] . includes ( config . MEMPOOL . NETWORK ) &&
config . DATABASE . ENABLED === true ;
try {
loadingIndicators . setProgress ( 'blocks' , 0 ) ;
let currentHeight = fromHeight ? fromHeight : this.getCurrentBlockHeight ( ) ;
const returnBlocks : BlockExtended [ ] = [ ] ;
if ( currentHeight < 0 ) {
return returnBlocks ;
}
// Check if block height exist in local cache to skip the hash lookup
const blockByHeight = this . getBlocks ( ) . find ( ( b ) = > b . height === currentHeight ) ;
let startFromHash : string | null = null ;
if ( blockByHeight ) {
startFromHash = blockByHeight . id ;
} else {
startFromHash = await bitcoinApi . $getBlockHash ( currentHeight ) ;
}
let nextHash = startFromHash ;
for ( let i = 0 ; i < 10 && currentHeight >= 0 ; i ++ ) {
let block = this . getBlocks ( ) . find ( ( b ) = > b . height === currentHeight ) ;
if ( ! block && indexingAvailable ) {
block = this . prepareBlock ( await this . $indexBlock ( currentHeight ) ) ;
} else if ( ! block ) {
block = this . prepareBlock ( await bitcoinApi . $getBlock ( nextHash ) ) ;
}
returnBlocks . push ( block ) ;
nextHash = block . previousblockhash ;
loadingIndicators . setProgress ( 'blocks' , i / 10 * 100 ) ;
currentHeight -- ;
}
return returnBlocks ;
} catch ( e ) {
loadingIndicators . setProgress ( 'blocks' , 100 ) ;
throw e ;
}
}
private prepareBlock ( block : any ) : BlockExtended {
return < BlockExtended > {
id : block.id ? ? block . hash , // hash for indexed block
timestamp : block?.timestamp ? ? block ? . blockTimestamp , // blockTimestamp for indexed block
height : block?.height ,
version : block?.version ,
bits : block?.bits ,
nonce : block?.nonce ,
difficulty : block?.difficulty ,
merkle_root : block?.merkle_root ,
tx_count : block?.tx_count ,
size : block?.size ,
weight : block?.weight ,
previousblockhash : block?.previousblockhash ,
extras : {
medianFee : block?.medianFee ,
feeRange : block?.feeRange ? ? [ ] , // TODO
reward : block?.reward ,
pool : block?.extras?.pool ? ? ( block ? . pool_id ? {
id : block?.pool_id ,
name : block?.pool_name ,
} : undefined ) ,
}
} ;
}
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 ( ) ;