Merge branch 'master' into nymkappa/scan-closed-channel-no-mempool
This commit is contained in:
@@ -44,7 +44,9 @@
|
||||
"TLS_ENABLED": true
|
||||
},
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "__ESPLORA_REST_API_URL__"
|
||||
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
||||
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
|
||||
"RETRY_UNIX_SOCKET_AFTER": "__ESPLORA_RETRY_UNIX_SOCKET_AFTER__"
|
||||
},
|
||||
"SECOND_CORE_RPC": {
|
||||
"HOST": "__SECOND_CORE_RPC_HOST__",
|
||||
@@ -60,7 +62,8 @@
|
||||
"PORT": 18,
|
||||
"DATABASE": "__DATABASE_DATABASE__",
|
||||
"USERNAME": "__DATABASE_USERNAME__",
|
||||
"PASSWORD": "__DATABASE_PASSWORD__"
|
||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||
"TIMEOUT": "__DATABASE_TIMEOUT__"
|
||||
},
|
||||
"SYSLOG": {
|
||||
"ENABLED": false,
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('Mempool Backend Config', () => {
|
||||
|
||||
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
||||
|
||||
expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000' });
|
||||
expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000', UNIX_SOCKET_PATH: null, RETRY_UNIX_SOCKET_AFTER: 30000 });
|
||||
|
||||
expect(config.CORE_RPC).toStrictEqual({
|
||||
HOST: '127.0.0.1',
|
||||
@@ -72,7 +72,8 @@ describe('Mempool Backend Config', () => {
|
||||
PORT: 3306,
|
||||
DATABASE: 'mempool',
|
||||
USERNAME: 'mempool',
|
||||
PASSWORD: 'mempool'
|
||||
PASSWORD: 'mempool',
|
||||
TIMEOUT: 180000,
|
||||
});
|
||||
|
||||
expect(config.SYSLOG).toStrictEqual({
|
||||
|
||||
@@ -93,17 +93,7 @@ class Audit {
|
||||
} else {
|
||||
if (!isDisplaced[tx.txid]) {
|
||||
added.push(tx.txid);
|
||||
} else {
|
||||
}
|
||||
let blockIndex = -1;
|
||||
let index = -1;
|
||||
projectedBlocks.forEach((block, bi) => {
|
||||
const i = block.transactionIds.indexOf(tx.txid);
|
||||
if (i >= 0) {
|
||||
blockIndex = bi;
|
||||
index = i;
|
||||
}
|
||||
});
|
||||
overflowWeight += tx.weight;
|
||||
}
|
||||
totalWeight += tx.weight;
|
||||
|
||||
@@ -32,8 +32,10 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', this.getRbfHistory)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/rbf', this.getRbfHistory)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||
try {
|
||||
@@ -94,6 +96,7 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
|
||||
@@ -110,7 +113,6 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock)
|
||||
@@ -128,8 +130,9 @@ class BitcoinRoutes {
|
||||
|
||||
private getInitData(req: Request, res: Response) {
|
||||
try {
|
||||
const result = websocketHandler.getInitData();
|
||||
res.json(result);
|
||||
const result = websocketHandler.getSerializedInitData();
|
||||
res.set('Content-Type', 'application/json');
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
@@ -589,10 +592,14 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async getBlockTipHeight(req: Request, res: Response) {
|
||||
private getBlockTipHeight(req: Request, res: Response) {
|
||||
try {
|
||||
const result = await bitcoinApi.$getBlockHeightTip();
|
||||
res.json(result);
|
||||
const result = blocks.getCurrentBlockHeight();
|
||||
if (!result) {
|
||||
return res.status(503).send(`Service Temporarily Unavailable`);
|
||||
}
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.send(result.toString());
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
@@ -638,8 +645,30 @@ class BitcoinRoutes {
|
||||
|
||||
private async getRbfHistory(req: Request, res: Response) {
|
||||
try {
|
||||
const result = rbfCache.getReplaces(req.params.txId);
|
||||
res.json(result || []);
|
||||
const replacements = rbfCache.getRbfTree(req.params.txId) || null;
|
||||
const replaces = rbfCache.getReplaces(req.params.txId) || null;
|
||||
res.json({
|
||||
replacements,
|
||||
replaces
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getRbfReplacements(req: Request, res: Response) {
|
||||
try {
|
||||
const result = rbfCache.getRbfTrees(false);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getFullRbfReplacements(req: Request, res: Response) {
|
||||
try {
|
||||
const result = rbfCache.getRbfTrees(true);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
@@ -3,65 +3,102 @@ import axios, { AxiosRequestConfig } from 'axios';
|
||||
import http from 'http';
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import logger from '../../logger';
|
||||
|
||||
const axiosConnection = axios.create({
|
||||
httpAgent: new http.Agent({ keepAlive: true })
|
||||
httpAgent: new http.Agent({ keepAlive: true, })
|
||||
});
|
||||
|
||||
class ElectrsApi implements AbstractBitcoinApi {
|
||||
axiosConfig: AxiosRequestConfig = {
|
||||
private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? {
|
||||
socketPath: config.ESPLORA.UNIX_SOCKET_PATH,
|
||||
timeout: 10000,
|
||||
} : {
|
||||
timeout: 10000,
|
||||
};
|
||||
private axiosConfigTcpSocketOnly: AxiosRequestConfig = {
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
constructor() { }
|
||||
unixSocketRetryTimeout;
|
||||
activeAxiosConfig;
|
||||
|
||||
constructor() {
|
||||
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
|
||||
}
|
||||
|
||||
fallbackToTcpSocket() {
|
||||
if (!this.unixSocketRetryTimeout) {
|
||||
logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`);
|
||||
// Retry the unix socket after a few seconds
|
||||
this.unixSocketRetryTimeout = setTimeout(() => {
|
||||
logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`);
|
||||
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
|
||||
this.unixSocketRetryTimeout = undefined;
|
||||
}, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER);
|
||||
}
|
||||
|
||||
// Use the TCP socket (reach a different esplora instance through nginx)
|
||||
this.activeAxiosConfig = this.axiosConfigTcpSocketOnly;
|
||||
}
|
||||
|
||||
$queryWrapper<T>(url, responseType = 'json'): Promise<T> {
|
||||
return axiosConnection.get<T>(url, { ...this.activeAxiosConfig, responseType: responseType })
|
||||
.then((response) => response.data)
|
||||
.catch((e) => {
|
||||
if (e?.code === 'ECONNREFUSED') {
|
||||
this.fallbackToTcpSocket();
|
||||
// Retry immediately
|
||||
return axiosConnection.get<T>(url, this.activeAxiosConfig)
|
||||
.then((response) => response.data)
|
||||
.catch((e) => {
|
||||
logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`);
|
||||
throw e;
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
||||
return axiosConnection.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids');
|
||||
}
|
||||
|
||||
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
|
||||
return axiosConnection.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
|
||||
}
|
||||
|
||||
$getTransactionHex(txId: string): Promise<string> {
|
||||
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
|
||||
}
|
||||
|
||||
$getBlockHeightTip(): Promise<number> {
|
||||
return axiosConnection.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height');
|
||||
}
|
||||
|
||||
$getBlockHashTip(): Promise<string> {
|
||||
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash');
|
||||
}
|
||||
|
||||
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||
return axiosConnection.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
|
||||
}
|
||||
|
||||
$getBlockHash(height: number): Promise<string> {
|
||||
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
|
||||
}
|
||||
|
||||
$getBlockHeader(hash: string): Promise<string> {
|
||||
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header');
|
||||
}
|
||||
|
||||
$getBlock(hash: string): Promise<IEsploraApi.Block> {
|
||||
return axiosConnection.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash);
|
||||
}
|
||||
|
||||
$getRawBlock(hash: string): Promise<Buffer> {
|
||||
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' })
|
||||
return this.$queryWrapper<any>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer')
|
||||
.then((response) => { return Buffer.from(response.data); });
|
||||
}
|
||||
|
||||
@@ -82,13 +119,11 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
}
|
||||
|
||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||
return axiosConnection.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout);
|
||||
}
|
||||
|
||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
||||
return axiosConnection.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
return this.$queryWrapper<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends');
|
||||
}
|
||||
|
||||
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
||||
|
||||
@@ -36,6 +36,8 @@ class Blocks {
|
||||
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
||||
private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>)[] = [];
|
||||
|
||||
private mainLoopTimeout: number = 120000;
|
||||
|
||||
constructor() { }
|
||||
|
||||
public getBlocks(): BlockExtended[] {
|
||||
@@ -410,12 +412,13 @@ class Blocks {
|
||||
try {
|
||||
// Get all indexed block hash
|
||||
const unindexedBlockHeights = await blocksRepository.$getCPFPUnindexedBlocks();
|
||||
logger.info(`Indexing cpfp data for ${unindexedBlockHeights.length} blocks`);
|
||||
|
||||
if (!unindexedBlockHeights?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Indexing cpfp data for ${unindexedBlockHeights.length} blocks`);
|
||||
|
||||
// Logging
|
||||
let count = 0;
|
||||
let countThisRun = 0;
|
||||
@@ -526,9 +529,16 @@ class Blocks {
|
||||
return await BlocksRepository.$validateChain();
|
||||
}
|
||||
|
||||
public async $updateBlocks() {
|
||||
public async $updateBlocks(): Promise<number> {
|
||||
// warn if this run stalls the main loop for more than 2 minutes
|
||||
const timer = this.startTimer();
|
||||
|
||||
diskCache.lock();
|
||||
|
||||
let fastForwarded = false;
|
||||
let handledBlocks = 0;
|
||||
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
||||
this.updateTimerProgress(timer, 'got block height tip');
|
||||
|
||||
if (this.blocks.length === 0) {
|
||||
this.currentBlockHeight = Math.max(blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT, -1);
|
||||
@@ -546,16 +556,21 @@ class Blocks {
|
||||
|
||||
if (!this.lastDifficultyAdjustmentTime) {
|
||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||
this.updateTimerProgress(timer, 'got blockchain info for initial difficulty adjustment');
|
||||
if (blockchainInfo.blocks === blockchainInfo.headers) {
|
||||
const heightDiff = blockHeightTip % 2016;
|
||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
||||
this.updateTimerProgress(timer, 'got block hash for initial difficulty adjustment');
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
||||
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
|
||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||
this.currentDifficulty = block.difficulty;
|
||||
|
||||
if (blockHeightTip >= 2016) {
|
||||
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
||||
this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment');
|
||||
const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash);
|
||||
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
|
||||
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
||||
logger.debug(`Initial difficulty adjustment data set.`);
|
||||
}
|
||||
@@ -570,9 +585,11 @@ class Blocks {
|
||||
} else {
|
||||
this.currentBlockHeight++;
|
||||
logger.debug(`New block found (#${this.currentBlockHeight})!`);
|
||||
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
|
||||
await chainTips.updateOrphanedBlocks();
|
||||
}
|
||||
|
||||
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);
|
||||
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
|
||||
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
|
||||
const block = BitcoinApi.convertBlock(verboseBlock);
|
||||
@@ -581,42 +598,53 @@ class Blocks {
|
||||
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
|
||||
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||
|
||||
// start async callbacks
|
||||
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
|
||||
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
if (!fastForwarded) {
|
||||
const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
|
||||
this.updateTimerProgress(timer, `got block by height for ${this.currentBlockHeight}`);
|
||||
if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock.id) {
|
||||
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`);
|
||||
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`, logger.tags.mining);
|
||||
// We assume there won't be a reorg with more than 10 block depth
|
||||
this.updateTimerProgress(timer, `rolling back diverged chain from ${this.currentBlockHeight}`);
|
||||
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
|
||||
await HashratesRepository.$deleteLastEntries();
|
||||
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock.height - 10);
|
||||
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
|
||||
this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`);
|
||||
for (let i = 10; i >= 0; --i) {
|
||||
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
||||
this.updateTimerProgress(timer, `reindexed block`);
|
||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
||||
this.updateTimerProgress(timer, `reindexed block summary`);
|
||||
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||
await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
||||
this.updateTimerProgress(timer, `reindexed block cpfp`);
|
||||
}
|
||||
}
|
||||
await mining.$indexDifficultyAdjustments();
|
||||
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
||||
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`);
|
||||
this.updateTimerProgress(timer, `reindexed difficulty adjustments`);
|
||||
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
|
||||
indexer.reindex();
|
||||
}
|
||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
|
||||
|
||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
|
||||
if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
|
||||
await blocksRepository.$saveBlockPrices([{
|
||||
height: blockExtended.height,
|
||||
priceId: lastestPriceId,
|
||||
}]);
|
||||
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
|
||||
} else {
|
||||
logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
|
||||
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
|
||||
setTimeout(() => {
|
||||
indexer.runSingleTask('blocksPrices');
|
||||
}, 10000);
|
||||
@@ -625,9 +653,11 @@ class Blocks {
|
||||
// Save blocks summary for visualization if it's enabled
|
||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
||||
this.updateTimerProgress(timer, `saved block summary for ${this.currentBlockHeight}`);
|
||||
}
|
||||
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||
this.$saveCpfp(blockExtended.id, this.currentBlockHeight, cpfpSummary);
|
||||
this.updateTimerProgress(timer, `saved cpfp for ${this.currentBlockHeight}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -640,6 +670,7 @@ class Blocks {
|
||||
difficulty: block.difficulty,
|
||||
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
|
||||
});
|
||||
this.updateTimerProgress(timer, `saved difficulty adjustment for ${this.currentBlockHeight}`);
|
||||
}
|
||||
|
||||
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
|
||||
@@ -664,7 +695,39 @@ class Blocks {
|
||||
}
|
||||
|
||||
// wait for pending async callbacks to finish
|
||||
this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`);
|
||||
await Promise.all(callbackPromises);
|
||||
this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`);
|
||||
|
||||
handledBlocks++;
|
||||
}
|
||||
|
||||
diskCache.unlock();
|
||||
|
||||
this.clearTimer(timer);
|
||||
|
||||
return handledBlocks;
|
||||
}
|
||||
|
||||
private startTimer() {
|
||||
const state: any = {
|
||||
start: Date.now(),
|
||||
progress: 'begin $updateBlocks',
|
||||
timer: null,
|
||||
};
|
||||
state.timer = setTimeout(() => {
|
||||
logger.err(`$updateBlocks stalled at "${state.progress}"`);
|
||||
}, this.mainLoopTimeout);
|
||||
return state;
|
||||
}
|
||||
|
||||
private updateTimerProgress(state, msg) {
|
||||
state.progress = msg;
|
||||
}
|
||||
|
||||
private clearTimer(state) {
|
||||
if (state.timer) {
|
||||
clearTimeout(state.timer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,7 +799,7 @@ class Blocks {
|
||||
|
||||
// Index the response if needed
|
||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||
await BlocksSummariesRepository.$saveSummary({height: block.height, mined: summary});
|
||||
await BlocksSummariesRepository.$saveTransactions(block.height, block.hash, summary.transactions);
|
||||
}
|
||||
|
||||
return summary.transactions;
|
||||
@@ -852,11 +915,12 @@ class Blocks {
|
||||
if (cleanBlock.fee_amt_percentiles === null) {
|
||||
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
|
||||
const summary = this.summarizeBlock(block);
|
||||
await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary });
|
||||
await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions);
|
||||
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
|
||||
}
|
||||
if (cleanBlock.fee_amt_percentiles !== null) {
|
||||
cleanBlock.median_fee_amt = cleanBlock.fee_amt_percentiles[3];
|
||||
await blocksRepository.$updateFeeAmounts(cleanBlock.hash, cleanBlock.fee_amt_percentiles, cleanBlock.median_fee_amt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,11 +57,11 @@ export class Common {
|
||||
return arr;
|
||||
}
|
||||
|
||||
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended } {
|
||||
const matches: { [txid: string]: TransactionExtended } = {};
|
||||
deleted
|
||||
.forEach((deletedTx) => {
|
||||
const foundMatches = added.find((addedTx) => {
|
||||
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended[] } {
|
||||
const matches: { [txid: string]: TransactionExtended[] } = {};
|
||||
added
|
||||
.forEach((addedTx) => {
|
||||
const foundMatches = deleted.filter((deletedTx) => {
|
||||
// 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.
|
||||
@@ -70,8 +70,8 @@ export class Common {
|
||||
&& deletedTx.vin.some((deletedVin) =>
|
||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||
});
|
||||
if (foundMatches) {
|
||||
matches[deletedTx.txid] = foundMatches;
|
||||
if (foundMatches?.length) {
|
||||
matches[addedTx.txid] = foundMatches;
|
||||
}
|
||||
});
|
||||
return matches;
|
||||
@@ -83,6 +83,7 @@ export class Common {
|
||||
fee: tx.fee,
|
||||
vsize: tx.weight / 4,
|
||||
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
|
||||
rate: tx.effectiveFeePerVsize,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,17 +7,26 @@ import logger from '../logger';
|
||||
import config from '../config';
|
||||
import { TransactionExtended } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
import rbfCache from './rbf-cache';
|
||||
|
||||
class DiskCache {
|
||||
private cacheSchemaVersion = 3;
|
||||
private rbfCacheSchemaVersion = 1;
|
||||
|
||||
private static TMP_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-cache.json';
|
||||
private static TMP_FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/tmp-cache{number}.json';
|
||||
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
|
||||
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
|
||||
private static TMP_RBF_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-rbfcache.json';
|
||||
private static RBF_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/rbfcache.json';
|
||||
private static CHUNK_FILES = 25;
|
||||
private isWritingCache = false;
|
||||
|
||||
private semaphore: { resume: (() => void)[], locks: number } = {
|
||||
resume: [],
|
||||
locks: 0,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
if (!cluster.isPrimary) {
|
||||
return;
|
||||
@@ -43,7 +52,7 @@ class DiskCache {
|
||||
const mempool = memPool.getMempool();
|
||||
const mempoolArray: TransactionExtended[] = [];
|
||||
for (const tx in mempool) {
|
||||
if (mempool[tx] && !mempool[tx].deleteAfter) {
|
||||
if (mempool[tx]) {
|
||||
mempoolArray.push(mempool[tx]);
|
||||
}
|
||||
}
|
||||
@@ -73,6 +82,7 @@ class DiskCache {
|
||||
fs.renameSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
|
||||
}
|
||||
} else {
|
||||
await this.$yield();
|
||||
await fsPromises.writeFile(DiskCache.TMP_FILE_NAME, JSON.stringify({
|
||||
network: config.MEMPOOL.NETWORK,
|
||||
cacheSchemaVersion: this.cacheSchemaVersion,
|
||||
@@ -82,6 +92,7 @@ class DiskCache {
|
||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
||||
}), { flag: 'w' });
|
||||
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
||||
await this.$yield();
|
||||
await fsPromises.writeFile(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
|
||||
mempool: {},
|
||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
||||
@@ -100,6 +111,32 @@ class DiskCache {
|
||||
logger.warn('Error writing to cache file: ' + (e instanceof Error ? e.message : e));
|
||||
this.isWritingCache = false;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug('Writing rbf data to disk cache (async)...');
|
||||
this.isWritingCache = true;
|
||||
const rbfData = rbfCache.dump();
|
||||
if (sync) {
|
||||
fs.writeFileSync(DiskCache.TMP_RBF_FILE_NAME, JSON.stringify({
|
||||
network: config.MEMPOOL.NETWORK,
|
||||
rbfCacheSchemaVersion: this.rbfCacheSchemaVersion,
|
||||
rbf: rbfData,
|
||||
}), { flag: 'w' });
|
||||
fs.renameSync(DiskCache.TMP_RBF_FILE_NAME, DiskCache.RBF_FILE_NAME);
|
||||
} else {
|
||||
await fsPromises.writeFile(DiskCache.TMP_RBF_FILE_NAME, JSON.stringify({
|
||||
network: config.MEMPOOL.NETWORK,
|
||||
rbfCacheSchemaVersion: this.rbfCacheSchemaVersion,
|
||||
rbf: rbfData,
|
||||
}), { flag: 'w' });
|
||||
await fsPromises.rename(DiskCache.TMP_RBF_FILE_NAME, DiskCache.RBF_FILE_NAME);
|
||||
}
|
||||
logger.debug('Rbf data saved to disk cache');
|
||||
this.isWritingCache = false;
|
||||
} catch (e) {
|
||||
logger.warn('Error writing rbf data to cache file: ' + (e instanceof Error ? e.message : e));
|
||||
this.isWritingCache = false;
|
||||
}
|
||||
}
|
||||
|
||||
wipeCache(): void {
|
||||
@@ -124,7 +161,19 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
|
||||
loadMempoolCache(): void {
|
||||
wipeRbfCache() {
|
||||
logger.notice(`Wipping nodejs backend cache/rbfcache.json file`);
|
||||
|
||||
try {
|
||||
fs.unlinkSync(DiskCache.RBF_FILE_NAME);
|
||||
} catch (e: any) {
|
||||
if (e?.code !== 'ENOENT') {
|
||||
logger.err(`Cannot wipe cache file ${DiskCache.RBF_FILE_NAME}. Exception ${JSON.stringify(e)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async $loadMempoolCache(): Promise<void> {
|
||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
return;
|
||||
}
|
||||
@@ -164,16 +213,65 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.info('Error parsing ' + fileName + '. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
logger.err('Error parsing ' + fileName + '. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
memPool.setMempool(data.mempool);
|
||||
await memPool.$setMempool(data.mempool);
|
||||
blocks.setBlocks(data.blocks);
|
||||
blocks.setBlockSummaries(data.blockSummaries || []);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
|
||||
try {
|
||||
let rbfData: any = {};
|
||||
const rbfCacheData = fs.readFileSync(DiskCache.RBF_FILE_NAME, 'utf8');
|
||||
if (rbfCacheData) {
|
||||
logger.info('Restoring rbf data from disk cache');
|
||||
rbfData = JSON.parse(rbfCacheData);
|
||||
if (rbfData.rbfCacheSchemaVersion === undefined || rbfData.rbfCacheSchemaVersion !== this.rbfCacheSchemaVersion) {
|
||||
logger.notice('Rbf disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
|
||||
return this.wipeRbfCache();
|
||||
}
|
||||
if (rbfData.network && rbfData.network !== config.MEMPOOL.NETWORK) {
|
||||
logger.notice('Rbf disk cache contains data from a different network. Clearing it and skipping the cache loading.');
|
||||
return this.wipeRbfCache();
|
||||
}
|
||||
}
|
||||
|
||||
if (rbfData?.rbf) {
|
||||
rbfCache.load(rbfData.rbf);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse rbf cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
private $yield(): Promise<void> {
|
||||
if (this.semaphore.locks) {
|
||||
logger.debug('Pause writing mempool and blocks data to disk cache (async)');
|
||||
return new Promise((resolve) => {
|
||||
this.semaphore.resume.push(resolve);
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
public lock(): void {
|
||||
this.semaphore.locks++;
|
||||
}
|
||||
|
||||
public unlock(): void {
|
||||
this.semaphore.locks = Math.max(0, this.semaphore.locks - 1);
|
||||
if (!this.semaphore.locks && this.semaphore.resume.length) {
|
||||
const nextResume = this.semaphore.resume.shift();
|
||||
if (nextResume) {
|
||||
logger.debug('Resume writing mempool and blocks data to disk cache (async)');
|
||||
nextResume();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
||||
import logger from '../../logger';
|
||||
|
||||
class Icons {
|
||||
private static FILE_NAME = './icons.json';
|
||||
private static FILE_NAME = '/elements/asset_registry_db/icons.json';
|
||||
private iconIds: string[] = [];
|
||||
private icons: { [assetId: string]: string; } = {};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logger from '../logger';
|
||||
import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces';
|
||||
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
import config from '../config';
|
||||
import { Worker } from 'worker_threads';
|
||||
@@ -10,6 +10,9 @@ class MempoolBlocks {
|
||||
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
||||
private txSelectionWorker: Worker | null = null;
|
||||
|
||||
private nextUid: number = 1;
|
||||
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
|
||||
|
||||
constructor() {}
|
||||
|
||||
public getMempoolBlocks(): MempoolBlock[] {
|
||||
@@ -54,7 +57,14 @@ class MempoolBlocks {
|
||||
});
|
||||
|
||||
// First sort
|
||||
memPoolArray.sort((a, b) => b.feePerVsize - a.feePerVsize);
|
||||
memPoolArray.sort((a, b) => {
|
||||
if (a.feePerVsize === b.feePerVsize) {
|
||||
// tie-break by lexicographic txid order for stability
|
||||
return a.txid < b.txid ? -1 : 1;
|
||||
} else {
|
||||
return b.feePerVsize - a.feePerVsize;
|
||||
}
|
||||
});
|
||||
|
||||
// Loop through and traverse all ancestors and sum up all the sizes + fees
|
||||
// Pass down size + fee to all unconfirmed children
|
||||
@@ -68,7 +78,14 @@ class MempoolBlocks {
|
||||
});
|
||||
|
||||
// Final sort, by effective fee
|
||||
memPoolArray.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
|
||||
memPoolArray.sort((a, b) => {
|
||||
if (a.effectiveFeePerVsize === b.effectiveFeePerVsize) {
|
||||
// tie-break by lexicographic txid order for stability
|
||||
return a.txid < b.txid ? -1 : 1;
|
||||
} else {
|
||||
return b.effectiveFeePerVsize - a.effectiveFeePerVsize;
|
||||
}
|
||||
});
|
||||
|
||||
const end = new Date().getTime();
|
||||
const time = end - start;
|
||||
@@ -87,21 +104,45 @@ class MempoolBlocks {
|
||||
|
||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
|
||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
let blockSize = 0;
|
||||
let blockWeight = 0;
|
||||
let blockVsize = 0;
|
||||
let blockFees = 0;
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
let transactionIds: string[] = [];
|
||||
let transactions: TransactionExtended[] = [];
|
||||
transactionsSorted.forEach((tx) => {
|
||||
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|
||||
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
|
||||
tx.position = {
|
||||
block: mempoolBlocks.length,
|
||||
vsize: blockVsize + (tx.vsize / 2),
|
||||
};
|
||||
blockWeight += tx.weight;
|
||||
transactions.push(tx);
|
||||
blockVsize += tx.vsize;
|
||||
blockSize += tx.size;
|
||||
blockFees += tx.fee;
|
||||
if (blockVsize <= sizeLimit) {
|
||||
transactions.push(tx);
|
||||
}
|
||||
transactionIds.push(tx.txid);
|
||||
} else {
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions));
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
|
||||
blockVsize = 0;
|
||||
tx.position = {
|
||||
block: mempoolBlocks.length,
|
||||
vsize: blockVsize + (tx.vsize / 2),
|
||||
};
|
||||
blockVsize += tx.vsize;
|
||||
blockWeight = tx.weight;
|
||||
blockSize = tx.size;
|
||||
blockFees = tx.fee;
|
||||
transactionIds = [tx.txid];
|
||||
transactions = [tx];
|
||||
}
|
||||
});
|
||||
if (transactions.length) {
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions));
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
|
||||
}
|
||||
|
||||
return mempoolBlocks;
|
||||
@@ -112,6 +153,7 @@ class MempoolBlocks {
|
||||
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
||||
let added: TransactionStripped[] = [];
|
||||
let removed: string[] = [];
|
||||
const changed: { txid: string, rate: number | undefined }[] = [];
|
||||
if (mempoolBlocks[i] && !prevBlocks[i]) {
|
||||
added = mempoolBlocks[i].transactions;
|
||||
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
|
||||
@@ -120,7 +162,7 @@ class MempoolBlocks {
|
||||
const prevIds = {};
|
||||
const newIds = {};
|
||||
prevBlocks[i].transactions.forEach(tx => {
|
||||
prevIds[tx.txid] = true;
|
||||
prevIds[tx.txid] = tx;
|
||||
});
|
||||
mempoolBlocks[i].transactions.forEach(tx => {
|
||||
newIds[tx.txid] = true;
|
||||
@@ -133,30 +175,43 @@ class MempoolBlocks {
|
||||
mempoolBlocks[i].transactions.forEach(tx => {
|
||||
if (!prevIds[tx.txid]) {
|
||||
added.push(tx);
|
||||
} else if (tx.rate !== prevIds[tx.txid].rate) {
|
||||
changed.push({ txid: tx.txid, rate: tx.rate });
|
||||
}
|
||||
});
|
||||
}
|
||||
mempoolBlockDeltas.push({
|
||||
added,
|
||||
removed
|
||||
removed,
|
||||
changed,
|
||||
});
|
||||
}
|
||||
return mempoolBlockDeltas;
|
||||
}
|
||||
|
||||
public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
||||
public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
||||
const start = Date.now();
|
||||
|
||||
// reset mempool short ids
|
||||
this.resetUids();
|
||||
for (const tx of Object.values(newMempool)) {
|
||||
this.setUid(tx);
|
||||
}
|
||||
|
||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||
// to reduce the overhead of passing this data to the worker thread
|
||||
const strippedMempool: { [txid: string]: ThreadTransaction } = {};
|
||||
Object.values(newMempool).filter(tx => !tx.deleteAfter).forEach(entry => {
|
||||
strippedMempool[entry.txid] = {
|
||||
txid: entry.txid,
|
||||
fee: entry.fee,
|
||||
weight: entry.weight,
|
||||
feePerVsize: entry.fee / (entry.weight / 4),
|
||||
effectiveFeePerVsize: entry.fee / (entry.weight / 4),
|
||||
vin: entry.vin.map(v => v.txid),
|
||||
};
|
||||
const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
|
||||
Object.values(newMempool).forEach(entry => {
|
||||
if (entry.uid != null) {
|
||||
strippedMempool.set(entry.uid, {
|
||||
uid: entry.uid,
|
||||
fee: entry.fee,
|
||||
weight: entry.weight,
|
||||
feePerVsize: entry.fee / (entry.weight / 4),
|
||||
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
|
||||
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// (re)initialize tx selection worker thread
|
||||
@@ -175,7 +230,7 @@ class MempoolBlocks {
|
||||
// run the block construction algorithm in a separate thread, and wait for a result
|
||||
let threadErrorListener;
|
||||
try {
|
||||
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
|
||||
const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
|
||||
threadErrorListener = reject;
|
||||
this.txSelectionWorker?.once('message', (result): void => {
|
||||
resolve(result);
|
||||
@@ -183,123 +238,151 @@ class MempoolBlocks {
|
||||
this.txSelectionWorker?.once('error', reject);
|
||||
});
|
||||
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
|
||||
let { blocks, clusters } = await workerResultPromise;
|
||||
// filter out stale transactions
|
||||
const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
|
||||
blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool)));
|
||||
const filteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
|
||||
if (filteredCount < unfilteredCount) {
|
||||
logger.warn(`tx selection worker thread returned ${unfilteredCount - filteredCount} stale transactions from makeBlockTemplates`);
|
||||
}
|
||||
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
|
||||
|
||||
// clean up thread error listener
|
||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||
|
||||
return this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
|
||||
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
||||
return processed;
|
||||
} catch (e) {
|
||||
logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
return this.mempoolBlocks;
|
||||
}
|
||||
|
||||
public async updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[], saveResults: boolean = false): Promise<void> {
|
||||
public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: TransactionExtended[], saveResults: boolean = false): Promise<void> {
|
||||
if (!this.txSelectionWorker) {
|
||||
// need to reset the worker
|
||||
this.makeBlockTemplates(newMempool, saveResults);
|
||||
await this.$makeBlockTemplates(newMempool, saveResults);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
for (const tx of Object.values(added)) {
|
||||
this.setUid(tx);
|
||||
}
|
||||
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
|
||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||
// to reduce the overhead of passing this data to the worker thread
|
||||
const addedStripped: ThreadTransaction[] = added.map(entry => {
|
||||
const addedStripped: CompactThreadTransaction[] = added.filter(entry => entry.uid != null).map(entry => {
|
||||
return {
|
||||
txid: entry.txid,
|
||||
uid: entry.uid || 0,
|
||||
fee: entry.fee,
|
||||
weight: entry.weight,
|
||||
feePerVsize: entry.fee / (entry.weight / 4),
|
||||
effectiveFeePerVsize: entry.fee / (entry.weight / 4),
|
||||
vin: entry.vin.map(v => v.txid),
|
||||
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
|
||||
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
|
||||
};
|
||||
});
|
||||
|
||||
// run the block construction algorithm in a separate thread, and wait for a result
|
||||
let threadErrorListener;
|
||||
try {
|
||||
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
|
||||
const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
|
||||
threadErrorListener = reject;
|
||||
this.txSelectionWorker?.once('message', (result): void => {
|
||||
resolve(result);
|
||||
});
|
||||
this.txSelectionWorker?.once('error', reject);
|
||||
});
|
||||
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed });
|
||||
let { blocks, clusters } = await workerResultPromise;
|
||||
// filter out stale transactions
|
||||
const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
|
||||
blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool)));
|
||||
const filteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
|
||||
if (filteredCount < unfilteredCount) {
|
||||
logger.warn(`tx selection worker thread returned ${unfilteredCount - filteredCount} stale transactions from updateBlockTemplates`);
|
||||
}
|
||||
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids });
|
||||
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
|
||||
|
||||
this.removeUids(removedUids);
|
||||
|
||||
// clean up thread error listener
|
||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||
|
||||
this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
|
||||
this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
|
||||
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
|
||||
} catch (e) {
|
||||
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
private processBlockTemplates(mempool, blocks, clusters, saveResults): MempoolBlockWithTransactions[] {
|
||||
// update this thread's mempool with the results
|
||||
blocks.forEach(block => {
|
||||
block.forEach(tx => {
|
||||
if (tx.txid && tx.txid in mempool) {
|
||||
if (tx.effectiveFeePerVsize != null) {
|
||||
mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
|
||||
}
|
||||
if (tx.cpfpRoot && tx.cpfpRoot in clusters) {
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
const cluster = clusters[tx.cpfpRoot];
|
||||
let matched = false;
|
||||
cluster.forEach(txid => {
|
||||
if (!txid || !mempool[txid]) {
|
||||
logger.warn('projected transaction ancestor missing from mempool cache');
|
||||
return;
|
||||
}
|
||||
if (txid === tx.txid) {
|
||||
matched = true;
|
||||
} else {
|
||||
const relative = {
|
||||
txid: txid,
|
||||
fee: mempool[txid].fee,
|
||||
weight: mempool[txid].weight,
|
||||
};
|
||||
if (matched) {
|
||||
descendants.push(relative);
|
||||
} else {
|
||||
ancestors.push(relative);
|
||||
}
|
||||
}
|
||||
});
|
||||
mempool[tx.txid].ancestors = ancestors;
|
||||
mempool[tx.txid].descendants = descendants;
|
||||
mempool[tx.txid].bestDescendant = null;
|
||||
}
|
||||
mempool[tx.txid].cpfpChecked = tx.cpfpChecked;
|
||||
} else {
|
||||
logger.warn('projected transaction missing from mempool cache');
|
||||
}
|
||||
});
|
||||
});
|
||||
private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, saveResults): MempoolBlockWithTransactions[] {
|
||||
for (const txid of Object.keys(rates)) {
|
||||
if (txid in mempool) {
|
||||
mempool[txid].effectiveFeePerVsize = rates[txid];
|
||||
}
|
||||
}
|
||||
|
||||
// unpack the condensed blocks into proper mempool blocks
|
||||
const mempoolBlocks = blocks.map((transactions) => {
|
||||
return this.dataToMempoolBlocks(transactions.map(tx => {
|
||||
return mempool[tx.txid] || null;
|
||||
}).filter(tx => !!tx));
|
||||
});
|
||||
const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees }[] = [];
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
// update this thread's mempool with the results
|
||||
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
||||
const block: string[] = blocks[blockIndex];
|
||||
let txid: string;
|
||||
let mempoolTx: TransactionExtended;
|
||||
let totalSize = 0;
|
||||
let totalVsize = 0;
|
||||
let totalWeight = 0;
|
||||
let totalFees = 0;
|
||||
const transactions: TransactionExtended[] = [];
|
||||
for (let txIndex = 0; txIndex < block.length; txIndex++) {
|
||||
txid = block[txIndex];
|
||||
if (txid) {
|
||||
mempoolTx = mempool[txid];
|
||||
// save position in projected blocks
|
||||
mempoolTx.position = {
|
||||
block: blockIndex,
|
||||
vsize: totalVsize + (mempoolTx.vsize / 2),
|
||||
};
|
||||
mempoolTx.cpfpChecked = true;
|
||||
|
||||
totalSize += mempoolTx.size;
|
||||
totalVsize += mempoolTx.vsize;
|
||||
totalWeight += mempoolTx.weight;
|
||||
totalFees += mempoolTx.fee;
|
||||
|
||||
if (totalVsize <= sizeLimit) {
|
||||
transactions.push(mempoolTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
readyBlocks.push({
|
||||
transactionIds: block,
|
||||
transactions,
|
||||
totalSize,
|
||||
totalWeight,
|
||||
totalFees
|
||||
});
|
||||
}
|
||||
|
||||
for (const cluster of Object.values(clusters)) {
|
||||
for (const memberTxid of cluster) {
|
||||
if (memberTxid in mempool) {
|
||||
const mempoolTx = mempool[memberTxid];
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
let matched = false;
|
||||
cluster.forEach(txid => {
|
||||
if (txid === memberTxid) {
|
||||
matched = true;
|
||||
} else {
|
||||
const relative = {
|
||||
txid: txid,
|
||||
fee: mempool[txid].fee,
|
||||
weight: mempool[txid].weight,
|
||||
};
|
||||
if (matched) {
|
||||
descendants.push(relative);
|
||||
} else {
|
||||
ancestors.push(relative);
|
||||
}
|
||||
}
|
||||
});
|
||||
mempoolTx.ancestors = ancestors;
|
||||
mempoolTx.descendants = descendants;
|
||||
mempoolTx.bestDescendant = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mempoolBlocks = readyBlocks.map(b => this.dataToMempoolBlocks(b.transactionIds, b.transactions, b.totalSize, b.totalWeight, b.totalFees));
|
||||
|
||||
if (saveResults) {
|
||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
||||
@@ -310,29 +393,69 @@ class MempoolBlocks {
|
||||
return mempoolBlocks;
|
||||
}
|
||||
|
||||
private dataToMempoolBlocks(transactions: TransactionExtended[]): MempoolBlockWithTransactions {
|
||||
let totalSize = 0;
|
||||
let totalWeight = 0;
|
||||
const fitTransactions: TransactionExtended[] = [];
|
||||
transactions.forEach(tx => {
|
||||
totalSize += tx.size;
|
||||
totalWeight += tx.weight;
|
||||
if ((totalWeight + tx.weight) <= config.MEMPOOL.BLOCK_WEIGHT_UNITS * 1.2) {
|
||||
fitTransactions.push(tx);
|
||||
}
|
||||
});
|
||||
private dataToMempoolBlocks(transactionIds: string[], transactions: TransactionExtended[], totalSize: number, totalWeight: number, totalFees: number): MempoolBlockWithTransactions {
|
||||
const feeStats = Common.calcEffectiveFeeStatistics(transactions);
|
||||
return {
|
||||
blockSize: totalSize,
|
||||
blockVSize: totalWeight / 4,
|
||||
nTx: transactions.length,
|
||||
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
|
||||
blockVSize: (totalWeight / 4), // fractional vsize to avoid rounding errors
|
||||
nTx: transactionIds.length,
|
||||
totalFees: totalFees,
|
||||
medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
|
||||
feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength),
|
||||
transactionIds: transactions.map((tx) => tx.txid),
|
||||
transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)),
|
||||
transactionIds: transactionIds,
|
||||
transactions: transactions.map((tx) => Common.stripTransaction(tx)),
|
||||
};
|
||||
}
|
||||
|
||||
private resetUids(): void {
|
||||
this.uidMap.clear();
|
||||
this.nextUid = 1;
|
||||
}
|
||||
|
||||
private setUid(tx: TransactionExtended): number {
|
||||
const uid = this.nextUid;
|
||||
this.nextUid++;
|
||||
this.uidMap.set(uid, tx.txid);
|
||||
tx.uid = uid;
|
||||
return uid;
|
||||
}
|
||||
|
||||
private getUid(tx: TransactionExtended): number | void {
|
||||
if (tx?.uid != null && this.uidMap.has(tx.uid)) {
|
||||
return tx.uid;
|
||||
}
|
||||
}
|
||||
|
||||
private removeUids(uids: number[]): void {
|
||||
for (const uid of uids) {
|
||||
this.uidMap.delete(uid);
|
||||
}
|
||||
}
|
||||
|
||||
private convertResultTxids({ blocks, rates, clusters }: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]>})
|
||||
: { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }} {
|
||||
const convertedBlocks: string[][] = blocks.map(block => block.map(uid => {
|
||||
return this.uidMap.get(uid) || '';
|
||||
}));
|
||||
const convertedRates = {};
|
||||
for (const rateUid of rates.keys()) {
|
||||
const rateTxid = this.uidMap.get(rateUid);
|
||||
if (rateTxid) {
|
||||
convertedRates[rateTxid] = rates.get(rateUid);
|
||||
}
|
||||
}
|
||||
const convertedClusters = {};
|
||||
for (const rootUid of clusters.keys()) {
|
||||
const rootTxid = this.uidMap.get(rootUid);
|
||||
if (rootTxid) {
|
||||
const members = clusters.get(rootUid)?.map(uid => {
|
||||
return this.uidMap.get(uid);
|
||||
});
|
||||
convertedClusters[rootTxid] = members;
|
||||
}
|
||||
}
|
||||
return { blocks: convertedBlocks, rates: convertedRates, clusters: convertedClusters } as { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }};
|
||||
}
|
||||
}
|
||||
|
||||
export default new MempoolBlocks();
|
||||
|
||||
@@ -11,8 +11,6 @@ import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
|
||||
class Mempool {
|
||||
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
|
||||
private static LAZY_DELETE_AFTER_SECONDS = 30;
|
||||
private inSync: boolean = false;
|
||||
private mempoolCacheDelta: number = -1;
|
||||
private mempoolCache: { [txId: string]: TransactionExtended } = {};
|
||||
@@ -20,7 +18,7 @@ class Mempool {
|
||||
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
||||
private asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||
deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined;
|
||||
|
||||
private txPerSecondArray: number[] = [];
|
||||
@@ -35,6 +33,7 @@ class Mempool {
|
||||
private SAMPLE_TIME = 10000; // In ms
|
||||
private timer = new Date().getTime();
|
||||
private missingTxCount = 0;
|
||||
private mainLoopTimeout: number = 120000;
|
||||
|
||||
constructor() {
|
||||
setInterval(this.updateTxPerSecond.bind(this), 1000);
|
||||
@@ -71,20 +70,20 @@ class Mempool {
|
||||
|
||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
|
||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) {
|
||||
this.asyncMempoolChangedCallback = fn;
|
||||
this.$asyncMempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
public getMempool(): { [txid: string]: TransactionExtended } {
|
||||
return this.mempoolCache;
|
||||
}
|
||||
|
||||
public setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
|
||||
public async $setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
|
||||
this.mempoolCache = mempoolData;
|
||||
if (this.mempoolChangedCallback) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
||||
}
|
||||
if (this.asyncMempoolChangedCallback) {
|
||||
this.asyncMempoolChangedCallback(this.mempoolCache, [], []);
|
||||
if (this.$asyncMempoolChangedCallback) {
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, [], []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,19 +116,23 @@ class Mempool {
|
||||
return txTimes;
|
||||
}
|
||||
|
||||
public async $updateMempool(): Promise<void> {
|
||||
public async $updateMempool(transactions: string[]): Promise<void> {
|
||||
logger.debug(`Updating mempool...`);
|
||||
|
||||
// warn if this run stalls the main loop for more than 2 minutes
|
||||
const timer = this.startTimer();
|
||||
|
||||
const start = new Date().getTime();
|
||||
let hasChange: boolean = false;
|
||||
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
||||
const transactions = await bitcoinApi.$getRawMempool();
|
||||
this.updateTimerProgress(timer, 'got raw mempool');
|
||||
const diff = transactions.length - currentMempoolSize;
|
||||
const newTransactions: TransactionExtended[] = [];
|
||||
|
||||
this.mempoolCacheDelta = Math.abs(diff);
|
||||
|
||||
if (!this.inSync) {
|
||||
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
|
||||
loadingIndicators.setProgress('mempool', currentMempoolSize / transactions.length * 100);
|
||||
}
|
||||
|
||||
// https://github.com/mempool/mempool/issues/3283
|
||||
@@ -142,10 +145,12 @@ class Mempool {
|
||||
}
|
||||
};
|
||||
|
||||
let loggerTimer = new Date().getTime() / 1000;
|
||||
for (const txid of transactions) {
|
||||
if (!this.mempoolCache[txid]) {
|
||||
try {
|
||||
const transaction = await transactionUtils.$getTransactionExtended(txid);
|
||||
this.updateTimerProgress(timer, 'fetched new transaction');
|
||||
this.mempoolCache[txid] = transaction;
|
||||
if (this.inSync) {
|
||||
this.txPerSecondArray.push(new Date().getTime());
|
||||
@@ -163,9 +168,12 @@ class Mempool {
|
||||
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
if ((new Date().getTime()) - start > Mempool.WEBSOCKET_REFRESH_RATE_MS) {
|
||||
break;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,13 +207,15 @@ class Mempool {
|
||||
const transactionsObject = {};
|
||||
transactions.forEach((txId) => transactionsObject[txId] = true);
|
||||
|
||||
// Flag transactions for lazy deletion
|
||||
// Delete evicted transactions from mempool
|
||||
for (const tx in this.mempoolCache) {
|
||||
if (!transactionsObject[tx] && !this.mempoolCache[tx].deleteAfter) {
|
||||
if (!transactionsObject[tx]) {
|
||||
deletedTransactions.push(this.mempoolCache[tx]);
|
||||
this.mempoolCache[tx].deleteAfter = new Date().getTime() + Mempool.LAZY_DELETE_AFTER_SECONDS * 1000;
|
||||
}
|
||||
}
|
||||
for (const tx of deletedTransactions) {
|
||||
delete this.mempoolCache[tx.txid];
|
||||
}
|
||||
}
|
||||
|
||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||
@@ -222,22 +232,46 @@ class Mempool {
|
||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
}
|
||||
if (this.asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
await this.asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||
}
|
||||
|
||||
const end = new Date().getTime();
|
||||
const time = end - start;
|
||||
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
|
||||
|
||||
this.clearTimer(timer);
|
||||
}
|
||||
|
||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
|
||||
private startTimer() {
|
||||
const state: any = {
|
||||
start: Date.now(),
|
||||
progress: 'begin $updateMempool',
|
||||
timer: null,
|
||||
};
|
||||
state.timer = setTimeout(() => {
|
||||
logger.err(`$updateMempool stalled at "${state.progress}"`);
|
||||
}, this.mainLoopTimeout);
|
||||
return state;
|
||||
}
|
||||
|
||||
private updateTimerProgress(state, msg) {
|
||||
state.progress = msg;
|
||||
}
|
||||
|
||||
private clearTimer(state) {
|
||||
if (state.timer) {
|
||||
clearTimeout(state.timer);
|
||||
}
|
||||
}
|
||||
|
||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended[]; }): void {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (this.mempoolCache[rbfTransaction]) {
|
||||
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
|
||||
// Store replaced transactions
|
||||
rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction].txid);
|
||||
// Erase the replaced transactions from the local mempool
|
||||
delete this.mempoolCache[rbfTransaction];
|
||||
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,17 +289,6 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
public deleteExpiredTransactions() {
|
||||
const now = new Date().getTime();
|
||||
for (const tx in this.mempoolCache) {
|
||||
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
|
||||
if (lazyDeleteAt && lazyDeleteAt < now) {
|
||||
delete this.mempoolCache[tx];
|
||||
rbfCache.evict(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private $getMempoolInfo() {
|
||||
if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) {
|
||||
return Promise.all([
|
||||
|
||||
@@ -452,7 +452,7 @@ class Mining {
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||
if (elapsedSeconds > 5) {
|
||||
const progress = Math.round(totalBlockChecked / blocks.length * 100);
|
||||
logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`, logger.tags.mining);
|
||||
logger.debug(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`, logger.tags.mining);
|
||||
timer = new Date().getTime() / 1000;
|
||||
}
|
||||
}
|
||||
@@ -558,8 +558,10 @@ class Mining {
|
||||
currentBlockHeight -= 10000;
|
||||
}
|
||||
|
||||
if (totalIndexed) {
|
||||
logger.info(`Indexing missing coinstatsindex data completed`, logger.tags.mining);
|
||||
if (totalIndexed > 0) {
|
||||
logger.info(`Indexing missing coinstatsindex data completed. Indexed ${totalIndexed}`, logger.tags.mining);
|
||||
} else {
|
||||
logger.debug(`Indexing missing coinstatsindex data completed. Indexed 0.`, logger.tags.mining);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +1,341 @@
|
||||
import { TransactionExtended } from "../mempool.interfaces";
|
||||
import logger from "../logger";
|
||||
import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import { Common } from "./common";
|
||||
|
||||
interface RbfTransaction extends TransactionStripped {
|
||||
rbf?: boolean;
|
||||
mined?: boolean;
|
||||
}
|
||||
|
||||
interface RbfTree {
|
||||
tx: RbfTransaction;
|
||||
time: number;
|
||||
interval?: number;
|
||||
mined?: boolean;
|
||||
fullRbf: boolean;
|
||||
replaces: RbfTree[];
|
||||
}
|
||||
|
||||
class RbfCache {
|
||||
private replacedBy: { [txid: string]: string; } = {};
|
||||
private replaces: { [txid: string]: string[] } = {};
|
||||
private txs: { [txid: string]: TransactionExtended } = {};
|
||||
private expiring: { [txid: string]: Date } = {};
|
||||
private replacedBy: Map<string, string> = new Map();
|
||||
private replaces: Map<string, string[]> = new Map();
|
||||
private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
|
||||
private dirtyTrees: Set<string> = new Set();
|
||||
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
|
||||
private txs: Map<string, TransactionExtended> = new Map();
|
||||
private expiring: Map<string, number> = new Map();
|
||||
|
||||
constructor() {
|
||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
|
||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||
}
|
||||
|
||||
public add(replacedTx: TransactionExtended, newTxId: string): void {
|
||||
this.replacedBy[replacedTx.txid] = newTxId;
|
||||
this.txs[replacedTx.txid] = replacedTx;
|
||||
if (!this.replaces[newTxId]) {
|
||||
this.replaces[newTxId] = [];
|
||||
public add(replaced: TransactionExtended[], newTxExtended: TransactionExtended): void {
|
||||
if (!newTxExtended || !replaced?.length) {
|
||||
return;
|
||||
}
|
||||
this.replaces[newTxId].push(replacedTx.txid);
|
||||
|
||||
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
|
||||
const newTime = newTxExtended.firstSeen || Date.now();
|
||||
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
||||
this.txs.set(newTx.txid, newTxExtended);
|
||||
|
||||
// maintain rbf trees
|
||||
let fullRbf = false;
|
||||
const replacedTrees: RbfTree[] = [];
|
||||
for (const replacedTxExtended of replaced) {
|
||||
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
|
||||
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
||||
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
||||
if (this.treeMap.has(replacedTx.txid)) {
|
||||
const treeId = this.treeMap.get(replacedTx.txid);
|
||||
if (treeId) {
|
||||
const tree = this.rbfTrees.get(treeId);
|
||||
this.rbfTrees.delete(treeId);
|
||||
if (tree) {
|
||||
tree.interval = newTime - tree?.time;
|
||||
replacedTrees.push(tree);
|
||||
fullRbf = fullRbf || tree.fullRbf;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const replacedTime = replacedTxExtended.firstSeen || Date.now();
|
||||
replacedTrees.push({
|
||||
tx: replacedTx,
|
||||
time: replacedTime,
|
||||
interval: newTime - replacedTime,
|
||||
fullRbf: !replacedTx.rbf,
|
||||
replaces: [],
|
||||
});
|
||||
fullRbf = fullRbf || !replacedTx.rbf;
|
||||
this.txs.set(replacedTx.txid, replacedTxExtended);
|
||||
}
|
||||
}
|
||||
const treeId = replacedTrees[0].tx.txid;
|
||||
const newTree = {
|
||||
tx: newTx,
|
||||
time: newTxExtended.firstSeen || Date.now(),
|
||||
fullRbf,
|
||||
replaces: replacedTrees
|
||||
};
|
||||
this.rbfTrees.set(treeId, newTree);
|
||||
this.updateTreeMap(treeId, newTree);
|
||||
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
||||
this.dirtyTrees.add(treeId);
|
||||
}
|
||||
|
||||
public getReplacedBy(txId: string): string | undefined {
|
||||
return this.replacedBy[txId];
|
||||
return this.replacedBy.get(txId);
|
||||
}
|
||||
|
||||
public getReplaces(txId: string): string[] | undefined {
|
||||
return this.replaces[txId];
|
||||
return this.replaces.get(txId);
|
||||
}
|
||||
|
||||
public getTx(txId: string): TransactionExtended | undefined {
|
||||
return this.txs[txId];
|
||||
return this.txs.get(txId);
|
||||
}
|
||||
|
||||
public getRbfTree(txId: string): RbfTree | void {
|
||||
return this.rbfTrees.get(this.treeMap.get(txId) || '');
|
||||
}
|
||||
|
||||
// get a paginated list of RbfTrees
|
||||
// ordered by most recent replacement time
|
||||
public getRbfTrees(onlyFullRbf: boolean, after?: string): RbfTree[] {
|
||||
const limit = 25;
|
||||
const trees: RbfTree[] = [];
|
||||
const used = new Set<string>();
|
||||
const replacements: string[][] = Array.from(this.replacedBy).reverse();
|
||||
const afterTree = after ? this.treeMap.get(after) : null;
|
||||
let ready = !afterTree;
|
||||
for (let i = 0; i < replacements.length && trees.length <= limit - 1; i++) {
|
||||
const txid = replacements[i][1];
|
||||
const treeId = this.treeMap.get(txid) || '';
|
||||
if (treeId === afterTree) {
|
||||
ready = true;
|
||||
} else if (ready) {
|
||||
if (!used.has(treeId)) {
|
||||
const tree = this.rbfTrees.get(treeId);
|
||||
used.add(treeId);
|
||||
if (tree && (!onlyFullRbf || tree.fullRbf)) {
|
||||
trees.push(tree);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return trees;
|
||||
}
|
||||
|
||||
// get map of rbf trees that have been updated since the last call
|
||||
public getRbfChanges(): { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} {
|
||||
const changes: { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} = {
|
||||
trees: {},
|
||||
map: {},
|
||||
};
|
||||
this.dirtyTrees.forEach(id => {
|
||||
const tree = this.rbfTrees.get(id);
|
||||
if (tree) {
|
||||
changes.trees[id] = tree;
|
||||
this.getTransactionsInTree(tree).forEach(tx => {
|
||||
changes.map[tx.txid] = id;
|
||||
});
|
||||
}
|
||||
});
|
||||
this.dirtyTrees = new Set();
|
||||
return changes;
|
||||
}
|
||||
|
||||
public mined(txid): void {
|
||||
if (!this.txs.has(txid)) {
|
||||
return;
|
||||
}
|
||||
const treeId = this.treeMap.get(txid);
|
||||
if (treeId && this.rbfTrees.has(treeId)) {
|
||||
const tree = this.rbfTrees.get(treeId);
|
||||
if (tree) {
|
||||
this.setTreeMined(tree, txid);
|
||||
tree.mined = true;
|
||||
this.dirtyTrees.add(treeId);
|
||||
}
|
||||
}
|
||||
this.evict(txid);
|
||||
}
|
||||
|
||||
// flag a transaction as removed from the mempool
|
||||
public evict(txid): void {
|
||||
this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours
|
||||
public evict(txid: string, fast: boolean = false): void {
|
||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||
this.expiring.set(txid, fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400)); // 24 hours
|
||||
}
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
const currentDate = new Date();
|
||||
for (const txid in this.expiring) {
|
||||
if (this.expiring[txid] < currentDate) {
|
||||
delete this.expiring[txid];
|
||||
const now = Date.now();
|
||||
for (const txid of this.expiring.keys()) {
|
||||
if ((this.expiring.get(txid) || 0) < now) {
|
||||
this.expiring.delete(txid);
|
||||
this.remove(txid);
|
||||
}
|
||||
}
|
||||
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.expiring.size} due to expire`);
|
||||
}
|
||||
|
||||
// remove a transaction & all previous versions from the cache
|
||||
private remove(txid): void {
|
||||
// don't remove a transaction while a newer version remains in the mempool
|
||||
if (this.replaces[txid] && !this.replacedBy[txid]) {
|
||||
const replaces = this.replaces[txid];
|
||||
delete this.replaces[txid];
|
||||
for (const tx of replaces) {
|
||||
// don't remove a transaction if a newer version remains in the mempool
|
||||
if (!this.replacedBy.has(txid)) {
|
||||
const replaces = this.replaces.get(txid);
|
||||
this.replaces.delete(txid);
|
||||
this.treeMap.delete(txid);
|
||||
this.txs.delete(txid);
|
||||
this.expiring.delete(txid);
|
||||
for (const tx of (replaces || [])) {
|
||||
// recursively remove prior versions from the cache
|
||||
delete this.replacedBy[tx];
|
||||
delete this.txs[tx];
|
||||
this.replacedBy.delete(tx);
|
||||
// if this is the id of a tree, remove that too
|
||||
if (this.treeMap.get(tx) === tx) {
|
||||
this.rbfTrees.delete(tx);
|
||||
}
|
||||
this.remove(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateTreeMap(newId: string, tree: RbfTree): void {
|
||||
this.treeMap.set(tree.tx.txid, newId);
|
||||
tree.replaces.forEach(subtree => {
|
||||
this.updateTreeMap(newId, subtree);
|
||||
});
|
||||
}
|
||||
|
||||
private getTransactionsInTree(tree: RbfTree, txs: RbfTransaction[] = []): RbfTransaction[] {
|
||||
txs.push(tree.tx);
|
||||
tree.replaces.forEach(subtree => {
|
||||
this.getTransactionsInTree(subtree, txs);
|
||||
});
|
||||
return txs;
|
||||
}
|
||||
|
||||
private setTreeMined(tree: RbfTree, txid: string): void {
|
||||
if (tree.tx.txid === txid) {
|
||||
tree.tx.mined = true;
|
||||
} else {
|
||||
tree.replaces.forEach(subtree => {
|
||||
this.setTreeMined(subtree, txid);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public dump(): any {
|
||||
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
|
||||
|
||||
return {
|
||||
txs: Array.from(this.txs.entries()),
|
||||
trees,
|
||||
expiring: Array.from(this.expiring.entries()),
|
||||
};
|
||||
}
|
||||
|
||||
public async load({ txs, trees, expiring }): Promise<void> {
|
||||
txs.forEach(txEntry => {
|
||||
this.txs.set(txEntry[0], txEntry[1]);
|
||||
});
|
||||
for (const deflatedTree of trees) {
|
||||
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
}
|
||||
expiring.forEach(expiringEntry => {
|
||||
if (this.txs.has(expiringEntry[0])) {
|
||||
this.expiring.set(expiringEntry[0], new Date(expiringEntry[1]).getTime());
|
||||
}
|
||||
});
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
exportTree(tree: RbfTree, deflated: any = null) {
|
||||
if (!deflated) {
|
||||
deflated = {
|
||||
root: tree.tx.txid,
|
||||
};
|
||||
}
|
||||
deflated[tree.tx.txid] = {
|
||||
tx: tree.tx.txid,
|
||||
txMined: tree.tx.mined,
|
||||
time: tree.time,
|
||||
interval: tree.interval,
|
||||
mined: tree.mined,
|
||||
fullRbf: tree.fullRbf,
|
||||
replaces: tree.replaces.map(child => child.tx.txid),
|
||||
};
|
||||
tree.replaces.forEach(child => {
|
||||
this.exportTree(child, deflated);
|
||||
});
|
||||
return deflated;
|
||||
}
|
||||
|
||||
async importTree(root, txid, deflated, txs: Map<string, TransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
|
||||
const treeInfo = deflated[txid];
|
||||
const replaces: RbfTree[] = [];
|
||||
|
||||
// check if any transactions in this tree have already been confirmed
|
||||
mined = mined || treeInfo.mined;
|
||||
let exists = mined;
|
||||
if (!mined) {
|
||||
try {
|
||||
const apiTx = await bitcoinApi.$getRawTransaction(txid);
|
||||
if (apiTx) {
|
||||
exists = true;
|
||||
}
|
||||
if (apiTx?.status?.confirmed) {
|
||||
mined = true;
|
||||
treeInfo.txMined = true;
|
||||
this.evict(txid, true);
|
||||
}
|
||||
} catch (e) {
|
||||
// most transactions do not exist
|
||||
}
|
||||
}
|
||||
|
||||
// if the root tx is not in the mempool or the blockchain
|
||||
// evict this tree as soon as possible
|
||||
if (root === txid && !exists) {
|
||||
this.evict(txid, true);
|
||||
}
|
||||
|
||||
// recursively reconstruct child trees
|
||||
for (const childId of treeInfo.replaces) {
|
||||
const replaced = await this.importTree(root, childId, deflated, txs, mined);
|
||||
if (replaced) {
|
||||
this.replacedBy.set(replaced.tx.txid, txid);
|
||||
replaces.push(replaced);
|
||||
if (replaced.mined) {
|
||||
mined = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.replaces.set(txid, replaces.map(t => t.tx.txid));
|
||||
|
||||
const tx = txs.get(txid);
|
||||
if (!tx) {
|
||||
return;
|
||||
}
|
||||
const strippedTx = Common.stripTransaction(tx) as RbfTransaction;
|
||||
strippedTx.rbf = tx.vin.some((v) => v.sequence < 0xfffffffe);
|
||||
strippedTx.mined = treeInfo.txMined;
|
||||
const tree = {
|
||||
tx: strippedTx,
|
||||
time: treeInfo.time,
|
||||
interval: treeInfo.interval,
|
||||
mined: mined,
|
||||
fullRbf: treeInfo.fullRbf,
|
||||
replaces,
|
||||
};
|
||||
this.treeMap.set(txid, root);
|
||||
if (root === txid) {
|
||||
this.rbfTrees.set(root, tree);
|
||||
this.dirtyTrees.add(root);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
|
||||
export default new RbfCache();
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import config from '../config';
|
||||
import logger from '../logger';
|
||||
import { ThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
|
||||
import { CompactThreadTransaction, AuditTransaction } from '../mempool.interfaces';
|
||||
import { PairingHeap } from '../utils/pairing-heap';
|
||||
import { Common } from './common';
|
||||
import { parentPort } from 'worker_threads';
|
||||
|
||||
let mempool: { [txid: string]: ThreadTransaction } = {};
|
||||
let mempool: Map<number, CompactThreadTransaction> = new Map();
|
||||
|
||||
if (parentPort) {
|
||||
parentPort.on('message', (params) => {
|
||||
@@ -13,18 +12,18 @@ if (parentPort) {
|
||||
mempool = params.mempool;
|
||||
} else if (params.type === 'update') {
|
||||
params.added.forEach(tx => {
|
||||
mempool[tx.txid] = tx;
|
||||
mempool.set(tx.uid, tx);
|
||||
});
|
||||
params.removed.forEach(txid => {
|
||||
delete mempool[txid];
|
||||
params.removed.forEach(uid => {
|
||||
mempool.delete(uid);
|
||||
});
|
||||
}
|
||||
|
||||
const { blocks, clusters } = makeBlockTemplates(mempool);
|
||||
const { blocks, rates, clusters } = makeBlockTemplates(mempool);
|
||||
|
||||
// return the result to main thread.
|
||||
if (parentPort) {
|
||||
parentPort.postMessage({ blocks, clusters });
|
||||
parentPort.postMessage({ blocks, rates, clusters });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -33,26 +32,25 @@ if (parentPort) {
|
||||
* Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core
|
||||
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
|
||||
*/
|
||||
function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
: { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } } {
|
||||
function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
||||
: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> } {
|
||||
const start = Date.now();
|
||||
const auditPool: { [txid: string]: AuditTransaction } = {};
|
||||
const auditPool: Map<number, AuditTransaction> = new Map();
|
||||
const mempoolArray: AuditTransaction[] = [];
|
||||
const restOfArray: ThreadTransaction[] = [];
|
||||
const cpfpClusters: { [root: string]: string[] } = {};
|
||||
const cpfpClusters: Map<number, number[]> = new Map();
|
||||
|
||||
// grab the top feerate txs up to maxWeight
|
||||
Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => {
|
||||
mempool.forEach(tx => {
|
||||
tx.dirty = false;
|
||||
// initializing everything up front helps V8 optimize property access later
|
||||
auditPool[tx.txid] = {
|
||||
txid: tx.txid,
|
||||
auditPool.set(tx.uid, {
|
||||
uid: tx.uid,
|
||||
fee: tx.fee,
|
||||
weight: tx.weight,
|
||||
feePerVsize: tx.feePerVsize,
|
||||
effectiveFeePerVsize: tx.feePerVsize,
|
||||
vin: tx.vin,
|
||||
inputs: tx.inputs || [],
|
||||
relativesSet: false,
|
||||
ancestorMap: new Map<string, AuditTransaction>(),
|
||||
ancestorMap: new Map<number, AuditTransaction>(),
|
||||
children: new Set<AuditTransaction>(),
|
||||
ancestorFee: 0,
|
||||
ancestorWeight: 0,
|
||||
@@ -60,8 +58,8 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
used: false,
|
||||
modified: false,
|
||||
modifiedNode: null,
|
||||
};
|
||||
mempoolArray.push(auditPool[tx.txid]);
|
||||
});
|
||||
mempoolArray.push(auditPool.get(tx.uid) as AuditTransaction);
|
||||
});
|
||||
|
||||
// Build relatives graph & calculate ancestor scores
|
||||
@@ -72,15 +70,28 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
}
|
||||
|
||||
// Sort by descending ancestor score
|
||||
mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0));
|
||||
mempoolArray.sort((a, b) => {
|
||||
if (b.score === a.score) {
|
||||
// tie-break by uid for stability
|
||||
return a.uid < b.uid ? -1 : 1;
|
||||
} else {
|
||||
return (b.score || 0) - (a.score || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Build blocks by greedily choosing the highest feerate package
|
||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||
const blocks: ThreadTransaction[][] = [];
|
||||
const blocks: number[][] = [];
|
||||
let blockWeight = 4000;
|
||||
let blockSize = 0;
|
||||
let transactions: AuditTransaction[] = [];
|
||||
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0));
|
||||
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
|
||||
if (a.score === b.score) {
|
||||
// tie-break by uid for stability
|
||||
return a.uid > b.uid;
|
||||
} else {
|
||||
return (a.score || 0) > (b.score || 0);
|
||||
}
|
||||
});
|
||||
let overflow: AuditTransaction[] = [];
|
||||
let failures = 0;
|
||||
let top = 0;
|
||||
@@ -107,30 +118,36 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
|
||||
if (nextTx && !nextTx?.used) {
|
||||
// Check if the package fits into this block
|
||||
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
||||
if (blocks.length >= 7 || (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS)) {
|
||||
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||
let isCluster = false;
|
||||
if (sortedTxSet.length > 1) {
|
||||
cpfpClusters[nextTx.txid] = sortedTxSet.map(tx => tx.txid);
|
||||
cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
|
||||
isCluster = true;
|
||||
}
|
||||
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
|
||||
const used: AuditTransaction[] = [];
|
||||
while (sortedTxSet.length) {
|
||||
const ancestor = sortedTxSet.pop();
|
||||
const mempoolTx = mempool[ancestor.txid];
|
||||
const mempoolTx = mempool.get(ancestor.uid);
|
||||
if (!mempoolTx) {
|
||||
continue;
|
||||
}
|
||||
ancestor.used = true;
|
||||
ancestor.usedBy = nextTx.txid;
|
||||
ancestor.usedBy = nextTx.uid;
|
||||
// update original copy of this tx with effective fee rate & relatives data
|
||||
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
|
||||
if (isCluster) {
|
||||
mempoolTx.cpfpRoot = nextTx.txid;
|
||||
if (mempoolTx.effectiveFeePerVsize !== effectiveFeeRate) {
|
||||
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
|
||||
mempoolTx.dirty = true;
|
||||
}
|
||||
if (mempoolTx.cpfpRoot !== nextTx.uid) {
|
||||
mempoolTx.cpfpRoot = isCluster ? nextTx.uid : null;
|
||||
mempoolTx.dirty;
|
||||
}
|
||||
mempoolTx.cpfpChecked = true;
|
||||
transactions.push(ancestor);
|
||||
blockSize += ancestor.size;
|
||||
blockWeight += ancestor.weight;
|
||||
used.push(ancestor);
|
||||
}
|
||||
@@ -156,11 +173,10 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
if ((exceededPackageTries || queueEmpty) && blocks.length < 7) {
|
||||
// construct this block
|
||||
if (transactions.length) {
|
||||
blocks.push(transactions.map(t => mempool[t.txid]));
|
||||
blocks.push(transactions.map(t => t.uid));
|
||||
}
|
||||
// reset for the next block
|
||||
transactions = [];
|
||||
blockSize = 0;
|
||||
blockWeight = 4000;
|
||||
|
||||
// 'overflow' packages didn't fit in this block, but are valid candidates for the next
|
||||
@@ -175,50 +191,38 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
overflow = [];
|
||||
}
|
||||
}
|
||||
// pack any leftover transactions into the last block
|
||||
for (const tx of overflow) {
|
||||
if (!tx || tx?.used) {
|
||||
continue;
|
||||
}
|
||||
blockWeight += tx.weight;
|
||||
const mempoolTx = mempool[tx.txid];
|
||||
// update original copy of this tx with effective fee rate & relatives data
|
||||
mempoolTx.effectiveFeePerVsize = tx.score;
|
||||
if (tx.ancestorMap.size > 0) {
|
||||
cpfpClusters[tx.txid] = Array.from(tx.ancestorMap?.values()).map(a => a.txid);
|
||||
mempoolTx.cpfpRoot = tx.txid;
|
||||
}
|
||||
mempoolTx.cpfpChecked = true;
|
||||
transactions.push(tx);
|
||||
tx.used = true;
|
||||
|
||||
if (overflow.length > 0) {
|
||||
logger.warn('GBT overflow list unexpectedly non-empty after final block constructed');
|
||||
}
|
||||
const blockTransactions = transactions.map(t => mempool[t.txid]);
|
||||
restOfArray.forEach(tx => {
|
||||
blockWeight += tx.weight;
|
||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
||||
tx.cpfpChecked = false;
|
||||
blockTransactions.push(tx);
|
||||
});
|
||||
if (blockTransactions.length) {
|
||||
blocks.push(blockTransactions);
|
||||
// add the final unbounded block if it contains any transactions
|
||||
if (transactions.length > 0) {
|
||||
blocks.push(transactions.map(t => t.uid));
|
||||
}
|
||||
|
||||
// get map of dirty transactions
|
||||
const rates = new Map<number, number>();
|
||||
for (const tx of mempool.values()) {
|
||||
if (tx?.dirty) {
|
||||
rates.set(tx.uid, tx.effectiveFeePerVsize || tx.feePerVsize);
|
||||
}
|
||||
}
|
||||
transactions = [];
|
||||
|
||||
const end = Date.now();
|
||||
const time = end - start;
|
||||
logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
|
||||
|
||||
return { blocks, clusters: cpfpClusters };
|
||||
return { blocks, rates, clusters: cpfpClusters };
|
||||
}
|
||||
|
||||
// traverse in-mempool ancestors
|
||||
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
||||
function setRelatives(
|
||||
tx: AuditTransaction,
|
||||
mempool: { [txid: string]: AuditTransaction },
|
||||
mempool: Map<number, AuditTransaction>,
|
||||
): void {
|
||||
for (const parent of tx.vin) {
|
||||
const parentTx = mempool[parent];
|
||||
for (const parent of tx.inputs) {
|
||||
const parentTx = mempool.get(parent);
|
||||
if (parentTx && !tx.ancestorMap?.has(parent)) {
|
||||
tx.ancestorMap.set(parent, parentTx);
|
||||
parentTx.children.add(tx);
|
||||
@@ -227,7 +231,7 @@ function setRelatives(
|
||||
setRelatives(parentTx, mempool);
|
||||
}
|
||||
parentTx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorMap.set(ancestor.txid, ancestor);
|
||||
tx.ancestorMap.set(ancestor.uid, ancestor);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -245,7 +249,7 @@ function setRelatives(
|
||||
// avoids recursion to limit call stack depth
|
||||
function updateDescendants(
|
||||
rootTx: AuditTransaction,
|
||||
mempool: { [txid: string]: AuditTransaction },
|
||||
mempool: Map<number, AuditTransaction>,
|
||||
modified: PairingHeap<AuditTransaction>,
|
||||
): void {
|
||||
const descendantSet: Set<AuditTransaction> = new Set();
|
||||
@@ -261,9 +265,9 @@ function updateDescendants(
|
||||
});
|
||||
while (descendants.length) {
|
||||
descendantTx = descendants.pop();
|
||||
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
||||
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.uid)) {
|
||||
// remove tx as ancestor
|
||||
descendantTx.ancestorMap.delete(rootTx.txid);
|
||||
descendantTx.ancestorMap.delete(rootTx.uid);
|
||||
descendantTx.ancestorFee -= rootTx.fee;
|
||||
descendantTx.ancestorWeight -= rootTx.weight;
|
||||
tmpScore = descendantTx.score;
|
||||
|
||||
@@ -26,6 +26,13 @@ class WebsocketHandler {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
private extraInitProperties = {};
|
||||
|
||||
private numClients = 0;
|
||||
private numConnected = 0;
|
||||
private numDisconnected = 0;
|
||||
|
||||
private initData: { [key: string]: string } = {};
|
||||
private serializedInitData: string = '{}';
|
||||
|
||||
constructor() { }
|
||||
|
||||
setWebsocketServer(wss: WebSocket.Server) {
|
||||
@@ -34,6 +41,41 @@ class WebsocketHandler {
|
||||
|
||||
setExtraInitProperties(property: string, value: any) {
|
||||
this.extraInitProperties[property] = value;
|
||||
this.setInitDataFields(this.extraInitProperties);
|
||||
}
|
||||
|
||||
private setInitDataFields(data: { [property: string]: any }): void {
|
||||
for (const property of Object.keys(data)) {
|
||||
if (data[property] != null) {
|
||||
this.initData[property] = JSON.stringify(data[property]);
|
||||
} else {
|
||||
delete this.initData[property];
|
||||
}
|
||||
}
|
||||
this.serializedInitData = '{'
|
||||
+ Object.keys(this.initData).map(key => `"${key}": ${this.initData[key]}`).join(', ')
|
||||
+ '}';
|
||||
}
|
||||
|
||||
private updateInitData(): void {
|
||||
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
this.setInitDataFields({
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||
'blocks': _blocks,
|
||||
'conversions': priceUpdater.getLatestPrices(),
|
||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||
'transactions': memPool.getLatestTransactions(),
|
||||
'backendInfo': backendInfo.getBackendInfo(),
|
||||
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
||||
'da': da?.previousTime ? da : undefined,
|
||||
'fees': feeApi.getRecommendedFee(),
|
||||
});
|
||||
}
|
||||
|
||||
public getSerializedInitData(): string {
|
||||
return this.serializedInitData;
|
||||
}
|
||||
|
||||
setupConnectionHandling() {
|
||||
@@ -42,7 +84,11 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
this.wss.on('connection', (client: WebSocket) => {
|
||||
this.numConnected++;
|
||||
client.on('error', logger.info);
|
||||
client.on('close', () => {
|
||||
this.numDisconnected++;
|
||||
});
|
||||
client.on('message', async (message: string) => {
|
||||
try {
|
||||
const parsedMessage: WebsocketResponse = JSON.parse(message);
|
||||
@@ -58,9 +104,10 @@ class WebsocketHandler {
|
||||
if (parsedMessage && parsedMessage['track-tx']) {
|
||||
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
|
||||
client['track-tx'] = parsedMessage['track-tx'];
|
||||
const trackTxid = client['track-tx'];
|
||||
// Client is telling the transaction wasn't found
|
||||
if (parsedMessage['watch-mempool']) {
|
||||
const rbfCacheTxid = rbfCache.getReplacedBy(client['track-tx']);
|
||||
const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid);
|
||||
if (rbfCacheTxid) {
|
||||
response['txReplaced'] = {
|
||||
txid: rbfCacheTxid,
|
||||
@@ -68,7 +115,7 @@ class WebsocketHandler {
|
||||
client['track-tx'] = null;
|
||||
} else {
|
||||
// It might have appeared before we had the time to start watching for it
|
||||
const tx = memPool.getMempool()[client['track-tx']];
|
||||
const tx = memPool.getMempool()[trackTxid];
|
||||
if (tx) {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
response['tx'] = tx;
|
||||
@@ -92,6 +139,13 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
const tx = memPool.getMempool()[trackTxid];
|
||||
if (tx && tx.position) {
|
||||
response['txPosition'] = {
|
||||
txid: trackTxid,
|
||||
position: tx.position,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
client['track-tx'] = null;
|
||||
}
|
||||
@@ -132,12 +186,22 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-rbf'] !== undefined) {
|
||||
if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) {
|
||||
client['track-rbf'] = parsedMessage['track-rbf'];
|
||||
} else {
|
||||
client['track-rbf'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage.action === 'init') {
|
||||
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||
if (!_blocks) {
|
||||
if (!this.initData['blocks']?.length || !this.initData['da']) {
|
||||
this.updateInitData();
|
||||
}
|
||||
if (!this.initData['blocks']?.length) {
|
||||
return;
|
||||
}
|
||||
client.send(JSON.stringify(this.getInitData(_blocks)));
|
||||
client.send(this.serializedInitData);
|
||||
}
|
||||
|
||||
if (parsedMessage.action === 'ping') {
|
||||
@@ -186,11 +250,14 @@ class WebsocketHandler {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.setInitDataFields({ 'loadingIndicators': indicators });
|
||||
|
||||
const response = JSON.stringify({ loadingIndicators: indicators });
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(JSON.stringify({ loadingIndicators: indicators }));
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,39 +266,28 @@ class WebsocketHandler {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.setInitDataFields({ 'conversions': conversionRates });
|
||||
|
||||
const response = JSON.stringify({ conversions: conversionRates });
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(JSON.stringify({ conversions: conversionRates }));
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
|
||||
getInitData(_blocks?: BlockExtended[]) {
|
||||
if (!_blocks) {
|
||||
_blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||
}
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
return {
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||
'blocks': _blocks,
|
||||
'conversions': priceUpdater.getLatestPrices(),
|
||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||
'transactions': memPool.getLatestTransactions(),
|
||||
'backendInfo': backendInfo.getBackendInfo(),
|
||||
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
||||
'da': da?.previousTime ? da : undefined,
|
||||
'fees': feeApi.getRecommendedFee(),
|
||||
...this.extraInitProperties
|
||||
};
|
||||
}
|
||||
|
||||
handleNewStatistic(stats: OptimizedStatistic) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.printLogs();
|
||||
|
||||
const response = JSON.stringify({
|
||||
'live-2h-chart': stats
|
||||
});
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
@@ -241,20 +297,20 @@ class WebsocketHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
client.send(JSON.stringify({
|
||||
'live-2h-chart': stats
|
||||
}));
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
|
||||
async handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
|
||||
async $handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
|
||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.printLogs();
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
await mempoolBlocks.updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid), true);
|
||||
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
|
||||
} else {
|
||||
mempoolBlocks.updateMempoolBlocks(newMempool, true);
|
||||
}
|
||||
@@ -266,8 +322,55 @@ class WebsocketHandler {
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
const rbfChanges = rbfCache.getRbfChanges();
|
||||
let rbfReplacements;
|
||||
let fullRbfReplacements;
|
||||
if (Object.keys(rbfChanges.trees).length) {
|
||||
rbfReplacements = rbfCache.getRbfTrees(false);
|
||||
fullRbfReplacements = rbfCache.getRbfTrees(true);
|
||||
}
|
||||
for (const deletedTx of deletedTransactions) {
|
||||
rbfCache.evict(deletedTx.txid);
|
||||
}
|
||||
const recommendedFees = feeApi.getRecommendedFee();
|
||||
|
||||
// update init data
|
||||
this.updateInitData();
|
||||
|
||||
// cache serialized objects to avoid stringify-ing the same thing for every client
|
||||
const responseCache = { ...this.initData };
|
||||
function getCachedResponse(key: string, data): string {
|
||||
if (!responseCache[key]) {
|
||||
responseCache[key] = JSON.stringify(data);
|
||||
}
|
||||
return responseCache[key];
|
||||
}
|
||||
|
||||
// pre-compute new tracked outspends
|
||||
const outspendCache: { [txid: string]: { [vout: number]: { vin: number, txid: string } } } = {};
|
||||
const trackedTxs = new Set<string>();
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client['track-tx']) {
|
||||
trackedTxs.add(client['track-tx']);
|
||||
}
|
||||
});
|
||||
if (trackedTxs.size > 0) {
|
||||
for (const tx of newTransactions) {
|
||||
for (let i = 0; i < tx.vin.length; i++) {
|
||||
const vin = tx.vin[i];
|
||||
if (trackedTxs.has(vin.txid)) {
|
||||
if (!outspendCache[vin.txid]) {
|
||||
outspendCache[vin.txid] = { [vin.vout]: { vin: i, txid: tx.txid }};
|
||||
} else {
|
||||
outspendCache[vin.txid][vin.vout] = { vin: i, txid: tx.txid };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
||||
|
||||
this.wss.clients.forEach(async (client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
@@ -276,17 +379,17 @@ class WebsocketHandler {
|
||||
const response = {};
|
||||
|
||||
if (client['want-stats']) {
|
||||
response['mempoolInfo'] = mempoolInfo;
|
||||
response['vBytesPerSecond'] = vBytesPerSecond;
|
||||
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
||||
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
|
||||
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', vBytesPerSecond);
|
||||
response['transactions'] = getCachedResponse('transactions', latestTransactions);
|
||||
if (da?.previousTime) {
|
||||
response['da'] = da;
|
||||
response['da'] = getCachedResponse('da', da);
|
||||
}
|
||||
response['fees'] = recommendedFees;
|
||||
response['fees'] = getCachedResponse('fees', recommendedFees);
|
||||
}
|
||||
|
||||
if (client['want-mempool-blocks']) {
|
||||
response['mempool-blocks'] = mBlocks;
|
||||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||
}
|
||||
|
||||
if (client['track-mempool-tx']) {
|
||||
@@ -295,12 +398,12 @@ class WebsocketHandler {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
response['tx'] = fullTx;
|
||||
response['tx'] = JSON.stringify(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
response['tx'] = tx;
|
||||
response['tx'] = JSON.stringify(tx);
|
||||
}
|
||||
client['track-mempool-tx'] = null;
|
||||
}
|
||||
@@ -340,7 +443,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (foundTransactions.length) {
|
||||
response['address-transactions'] = foundTransactions;
|
||||
response['address-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,49 +472,60 @@ class WebsocketHandler {
|
||||
});
|
||||
|
||||
if (foundTransactions.length) {
|
||||
response['address-transactions'] = foundTransactions;
|
||||
response['address-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-tx']) {
|
||||
const outspends: object = {};
|
||||
newTransactions.forEach((tx) => tx.vin.forEach((vin, i) => {
|
||||
if (vin.txid === client['track-tx']) {
|
||||
outspends[vin.vout] = {
|
||||
vin: i,
|
||||
txid: tx.txid,
|
||||
};
|
||||
}
|
||||
}));
|
||||
const trackTxid = client['track-tx'];
|
||||
const outspends = outspendCache[trackTxid];
|
||||
|
||||
if (Object.keys(outspends).length) {
|
||||
response['utxoSpent'] = outspends;
|
||||
if (outspends && Object.keys(outspends).length) {
|
||||
response['utxoSpent'] = JSON.stringify(outspends);
|
||||
}
|
||||
|
||||
if (rbfTransactions[client['track-tx']]) {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (client['track-tx'] === rbfTransaction) {
|
||||
response['rbfTransaction'] = {
|
||||
txid: rbfTransactions[rbfTransaction].txid,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
const rbfReplacedBy = rbfCache.getReplacedBy(client['track-tx']);
|
||||
if (rbfReplacedBy) {
|
||||
response['rbfTransaction'] = JSON.stringify({
|
||||
txid: rbfReplacedBy,
|
||||
})
|
||||
}
|
||||
|
||||
const rbfChange = rbfChanges.map[client['track-tx']];
|
||||
if (rbfChange) {
|
||||
response['rbfInfo'] = JSON.stringify(rbfChanges.trees[rbfChange]);
|
||||
}
|
||||
|
||||
const mempoolTx = newMempool[trackTxid];
|
||||
if (mempoolTx && mempoolTx.position) {
|
||||
response['txPosition'] = JSON.stringify({
|
||||
txid: trackTxid,
|
||||
position: mempoolTx.position,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-block'] >= 0) {
|
||||
const index = client['track-mempool-block'];
|
||||
if (mBlockDeltas[index]) {
|
||||
response['projected-block-transactions'] = {
|
||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
||||
index: index,
|
||||
delta: mBlockDeltas[index],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-rbf'] === 'all' && rbfReplacements) {
|
||||
response['rbfLatest'] = getCachedResponse('rbfLatest', rbfReplacements);
|
||||
} else if (client['track-rbf'] === 'fullRbf' && fullRbfReplacements) {
|
||||
response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements);
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(JSON.stringify(response));
|
||||
const serializedResponse = '{'
|
||||
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
||||
+ '}';
|
||||
client.send(serializedResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -421,17 +535,25 @@ class WebsocketHandler {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.printLogs();
|
||||
|
||||
const _memPool = memPool.getMempool();
|
||||
|
||||
if (config.MEMPOOL.AUDIT) {
|
||||
let projectedBlocks;
|
||||
let auditMempool = _memPool;
|
||||
// template calculation functions have mempool side effects, so calculate audits using
|
||||
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
|
||||
const auditMempool = (config.MEMPOOL.ADVANCED_GBT_AUDIT === config.MEMPOOL.ADVANCED_GBT_MEMPOOL) ? _memPool : deepClone(_memPool);
|
||||
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||
projectedBlocks = await mempoolBlocks.makeBlockTemplates(auditMempool, false);
|
||||
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
|
||||
if (separateAudit) {
|
||||
auditMempool = deepClone(_memPool);
|
||||
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
|
||||
}
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
}
|
||||
|
||||
if (Common.indexingEnabled() && memPool.isInSync()) {
|
||||
@@ -477,16 +599,14 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
const removed: string[] = [];
|
||||
// Update mempool to remove transactions included in the new block
|
||||
for (const txId of txIds) {
|
||||
delete _memPool[txId];
|
||||
removed.push(txId);
|
||||
rbfCache.evict(txId);
|
||||
rbfCache.mined(txId);
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
await mempoolBlocks.updateBlockTemplates(_memPool, [], removed, true);
|
||||
await mempoolBlocks.$makeBlockTemplates(_memPool, true);
|
||||
} else {
|
||||
mempoolBlocks.updateMempoolBlocks(_memPool, true);
|
||||
}
|
||||
@@ -496,6 +616,19 @@ class WebsocketHandler {
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
const fees = feeApi.getRecommendedFee();
|
||||
|
||||
// update init data
|
||||
this.updateInitData();
|
||||
|
||||
const responseCache = { ...this.initData };
|
||||
function getCachedResponse(key, data): string {
|
||||
if (!responseCache[key]) {
|
||||
responseCache[key] = JSON.stringify(data);
|
||||
}
|
||||
return responseCache[key];
|
||||
}
|
||||
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
@@ -505,19 +638,29 @@ class WebsocketHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = {
|
||||
'block': block,
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'da': da?.previousTime ? da : undefined,
|
||||
'fees': fees,
|
||||
};
|
||||
const response = {};
|
||||
response['block'] = getCachedResponse('block', block);
|
||||
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
|
||||
response['da'] = getCachedResponse('da', da?.previousTime ? da : undefined);
|
||||
response['fees'] = getCachedResponse('fees', fees);
|
||||
|
||||
if (mBlocks && client['want-mempool-blocks']) {
|
||||
response['mempool-blocks'] = mBlocks;
|
||||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||
}
|
||||
|
||||
if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) {
|
||||
response['txConfirmed'] = true;
|
||||
if (client['track-tx']) {
|
||||
const trackTxid = client['track-tx'];
|
||||
if (txIds.indexOf(trackTxid) > -1) {
|
||||
response['txConfirmed'] = 'true';
|
||||
} else {
|
||||
const mempoolTx = _memPool[trackTxid];
|
||||
if (mempoolTx && mempoolTx.position) {
|
||||
response['txPosition'] = JSON.stringify({
|
||||
txid: trackTxid,
|
||||
position: mempoolTx.position,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-address']) {
|
||||
@@ -543,7 +686,7 @@ class WebsocketHandler {
|
||||
};
|
||||
});
|
||||
|
||||
response['block-transactions'] = foundTransactions;
|
||||
response['block-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,23 +723,37 @@ class WebsocketHandler {
|
||||
};
|
||||
});
|
||||
|
||||
response['block-transactions'] = foundTransactions;
|
||||
response['block-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-block'] >= 0) {
|
||||
const index = client['track-mempool-block'];
|
||||
if (mBlockDeltas && mBlockDeltas[index]) {
|
||||
response['projected-block-transactions'] = {
|
||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
||||
index: index,
|
||||
delta: mBlockDeltas[index],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
client.send(JSON.stringify(response));
|
||||
const serializedResponse = '{'
|
||||
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
||||
+ '}';
|
||||
client.send(serializedResponse);
|
||||
});
|
||||
}
|
||||
|
||||
private printLogs(): void {
|
||||
if (this.wss) {
|
||||
const count = this.wss?.clients?.size || 0;
|
||||
const diff = count - this.numClients;
|
||||
this.numClients = count;
|
||||
logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`);
|
||||
this.numConnected = 0;
|
||||
this.numDisconnected = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebsocketHandler();
|
||||
|
||||
@@ -37,6 +37,8 @@ interface IConfig {
|
||||
};
|
||||
ESPLORA: {
|
||||
REST_API_URL: string;
|
||||
UNIX_SOCKET_PATH: string | void | null;
|
||||
RETRY_UNIX_SOCKET_AFTER: number;
|
||||
};
|
||||
LIGHTNING: {
|
||||
ENABLED: boolean;
|
||||
@@ -84,6 +86,7 @@ interface IConfig {
|
||||
DATABASE: string;
|
||||
USERNAME: string;
|
||||
PASSWORD: string;
|
||||
TIMEOUT: number;
|
||||
};
|
||||
SYSLOG: {
|
||||
ENABLED: boolean;
|
||||
@@ -163,6 +166,8 @@ const defaults: IConfig = {
|
||||
},
|
||||
'ESPLORA': {
|
||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||
'UNIX_SOCKET_PATH': null,
|
||||
'RETRY_UNIX_SOCKET_AFTER': 30000,
|
||||
},
|
||||
'ELECTRUM': {
|
||||
'HOST': '127.0.0.1',
|
||||
@@ -190,7 +195,8 @@ const defaults: IConfig = {
|
||||
'PORT': 3306,
|
||||
'DATABASE': 'mempool',
|
||||
'USERNAME': 'mempool',
|
||||
'PASSWORD': 'mempool'
|
||||
'PASSWORD': 'mempool',
|
||||
'TIMEOUT': 180000,
|
||||
},
|
||||
'SYSLOG': {
|
||||
'ENABLED': true,
|
||||
|
||||
@@ -33,8 +33,32 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
OkPacket[] | ResultSetHeader>(query, params?): Promise<[T, FieldPacket[]]>
|
||||
{
|
||||
this.checkDBFlag();
|
||||
const pool = await this.getPool();
|
||||
return pool.query(query, params);
|
||||
let hardTimeout;
|
||||
if (query?.timeout != null) {
|
||||
hardTimeout = Math.floor(query.timeout * 1.1);
|
||||
} else {
|
||||
hardTimeout = config.DATABASE.TIMEOUT;
|
||||
}
|
||||
if (hardTimeout > 0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`DB query failed to return, reject or time out within ${hardTimeout / 1000}s - ${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}`));
|
||||
}, hardTimeout);
|
||||
|
||||
this.getPool().then(pool => {
|
||||
return pool.query(query, params) as Promise<[T, FieldPacket[]]>;
|
||||
}).then(result => {
|
||||
resolve(result);
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const pool = await this.getPool();
|
||||
return pool.query(query, params);
|
||||
}
|
||||
}
|
||||
|
||||
public async checkDbConnection() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import express from 'express';
|
||||
import { Application, Request, Response, NextFunction } from 'express';
|
||||
import * as http from 'http';
|
||||
import * as WebSocket from 'ws';
|
||||
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
|
||||
import cluster from 'cluster';
|
||||
import DB from './database';
|
||||
import config from './config';
|
||||
@@ -45,7 +46,8 @@ class Server {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
private server: http.Server | undefined;
|
||||
private app: Application;
|
||||
private currentBackendRetryInterval = 5;
|
||||
private currentBackendRetryInterval = 1;
|
||||
private backendRetryCount = 0;
|
||||
|
||||
private maxHeapSize: number = 0;
|
||||
private heapLogInterval: number = 60;
|
||||
@@ -120,7 +122,7 @@ class Server {
|
||||
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
|
||||
await syncAssets.syncAssets$();
|
||||
if (config.MEMPOOL.ENABLED) {
|
||||
diskCache.loadMempoolCache();
|
||||
await diskCache.$loadMempoolCache();
|
||||
}
|
||||
|
||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
|
||||
@@ -178,23 +180,26 @@ class Server {
|
||||
logger.debug(msg);
|
||||
}
|
||||
}
|
||||
memPool.deleteExpiredTransactions();
|
||||
await blocks.$updateBlocks();
|
||||
await memPool.$updateMempool();
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool);
|
||||
}
|
||||
indexer.$run();
|
||||
|
||||
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
|
||||
this.currentBackendRetryInterval = 5;
|
||||
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
|
||||
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 1 : config.MEMPOOL.POLL_RATE_MS);
|
||||
this.backendRetryCount = 0;
|
||||
} catch (e: any) {
|
||||
let loggerMsg = `Exception in runMainUpdateLoop(). Retrying in ${this.currentBackendRetryInterval} sec.`;
|
||||
this.backendRetryCount++;
|
||||
let loggerMsg = `Exception in runMainUpdateLoop() (count: ${this.backendRetryCount}). Retrying in ${this.currentBackendRetryInterval} sec.`;
|
||||
loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`;
|
||||
if (e?.stack) {
|
||||
loggerMsg += ` Stack trace: ${e.stack}`;
|
||||
}
|
||||
// When we get a first Exception, only `logger.debug` it and retry after 5 seconds
|
||||
// From the second Exception, `logger.warn` the Exception and increase the retry delay
|
||||
// Maximum retry delay is 60 seconds
|
||||
if (this.currentBackendRetryInterval > 5) {
|
||||
if (this.backendRetryCount >= 5) {
|
||||
logger.warn(loggerMsg);
|
||||
mempool.setOutOfSync();
|
||||
} else {
|
||||
@@ -204,8 +209,8 @@ class Server {
|
||||
logger.debug(`AxiosError: ${e?.message}`);
|
||||
}
|
||||
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
|
||||
this.currentBackendRetryInterval *= 2;
|
||||
this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60);
|
||||
} finally {
|
||||
diskCache.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +243,7 @@ class Server {
|
||||
websocketHandler.setupConnectionHandling();
|
||||
if (config.MEMPOOL.ENABLED) {
|
||||
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
|
||||
memPool.setAsyncMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
|
||||
memPool.setAsyncMempoolChangedCallback(websocketHandler.$handleMempoolChange.bind(websocketHandler));
|
||||
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
||||
}
|
||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
@@ -276,7 +281,7 @@ class Server {
|
||||
|
||||
if (!this.warnedHeapCritical && this.maxHeapSize > warnThreshold) {
|
||||
this.warnedHeapCritical = true;
|
||||
logger.warn(`Used ${(this.maxHeapSize / stats.heap_size_limit).toFixed(2)}% of heap limit (${formatBytes(this.maxHeapSize, byteUnits, true)} / ${formatBytes(stats.heap_size_limit, byteUnits)})!`);
|
||||
logger.warn(`Used ${(this.maxHeapSize / stats.heap_size_limit * 100).toFixed(2)}% of heap limit (${formatBytes(this.maxHeapSize, byteUnits, true)} / ${formatBytes(stats.heap_size_limit, byteUnits)})!`);
|
||||
}
|
||||
if (this.lastHeapLogTime === null || (now - this.lastHeapLogTime) > (this.heapLogInterval * 1000)) {
|
||||
logger.debug(`Memory usage: ${formatBytes(this.maxHeapSize, byteUnits)} / ${formatBytes(stats.heap_size_limit, byteUnits)}`);
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
|
||||
export interface MempoolBlockDelta {
|
||||
added: TransactionStripped[];
|
||||
removed: string[];
|
||||
changed: { txid: string, rate: number | undefined }[];
|
||||
}
|
||||
|
||||
interface VinStrippedToScriptsig {
|
||||
@@ -79,18 +80,22 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
||||
descendants?: Ancestor[];
|
||||
bestDescendant?: BestDescendant | null;
|
||||
cpfpChecked?: boolean;
|
||||
deleteAfter?: number;
|
||||
position?: {
|
||||
block: number,
|
||||
vsize: number,
|
||||
};
|
||||
uid?: number;
|
||||
}
|
||||
|
||||
export interface AuditTransaction {
|
||||
txid: string;
|
||||
uid: number;
|
||||
fee: number;
|
||||
weight: number;
|
||||
feePerVsize: number;
|
||||
effectiveFeePerVsize: number;
|
||||
vin: string[];
|
||||
inputs: number[];
|
||||
relativesSet: boolean;
|
||||
ancestorMap: Map<string, AuditTransaction>;
|
||||
ancestorMap: Map<number, AuditTransaction>;
|
||||
children: Set<AuditTransaction>;
|
||||
ancestorFee: number;
|
||||
ancestorWeight: number;
|
||||
@@ -100,13 +105,25 @@ export interface AuditTransaction {
|
||||
modifiedNode: HeapNode<AuditTransaction>;
|
||||
}
|
||||
|
||||
export interface CompactThreadTransaction {
|
||||
uid: number;
|
||||
fee: number;
|
||||
weight: number;
|
||||
feePerVsize: number;
|
||||
effectiveFeePerVsize?: number;
|
||||
inputs: number[];
|
||||
cpfpRoot?: string;
|
||||
cpfpChecked?: boolean;
|
||||
dirty?: boolean;
|
||||
}
|
||||
|
||||
export interface ThreadTransaction {
|
||||
txid: string;
|
||||
fee: number;
|
||||
weight: number;
|
||||
feePerVsize: number;
|
||||
effectiveFeePerVsize?: number;
|
||||
vin: string[];
|
||||
inputs: number[];
|
||||
cpfpRoot?: string;
|
||||
cpfpChecked?: boolean;
|
||||
}
|
||||
@@ -145,6 +162,7 @@ export interface TransactionStripped {
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
rate?: number; // effective fee rate
|
||||
}
|
||||
|
||||
export interface BlockExtension {
|
||||
|
||||
@@ -13,6 +13,48 @@ import chainTips from '../api/chain-tips';
|
||||
import blocks from '../api/blocks';
|
||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||
|
||||
interface DatabaseBlock {
|
||||
id: string;
|
||||
height: number;
|
||||
version: number;
|
||||
timestamp: number;
|
||||
bits: number;
|
||||
nonce: number;
|
||||
difficulty: number;
|
||||
merkle_root: string;
|
||||
tx_count: number;
|
||||
size: number;
|
||||
weight: number;
|
||||
previousblockhash: string;
|
||||
mediantime: number;
|
||||
totalFees: number;
|
||||
medianFee: number;
|
||||
feeRange: string;
|
||||
reward: number;
|
||||
poolId: number;
|
||||
poolName: string;
|
||||
poolSlug: string;
|
||||
avgFee: number;
|
||||
avgFeeRate: number;
|
||||
coinbaseRaw: string;
|
||||
coinbaseAddress: string;
|
||||
coinbaseSignature: string;
|
||||
coinbaseSignatureAscii: string;
|
||||
avgTxSize: number;
|
||||
totalInputs: number;
|
||||
totalOutputs: number;
|
||||
totalOutputAmt: number;
|
||||
medianFeeAmt: number;
|
||||
feePercentiles: string;
|
||||
segwitTotalTxs: number;
|
||||
segwitTotalSize: number;
|
||||
segwitTotalWeight: number;
|
||||
header: string;
|
||||
utxoSetChange: number;
|
||||
utxoSetSize: number;
|
||||
totalInputAmt: number;
|
||||
}
|
||||
|
||||
const BLOCK_DB_FIELDS = `
|
||||
blocks.hash AS id,
|
||||
blocks.height,
|
||||
@@ -52,7 +94,7 @@ const BLOCK_DB_FIELDS = `
|
||||
blocks.header,
|
||||
blocks.utxoset_change AS utxoSetChange,
|
||||
blocks.utxoset_size AS utxoSetSize,
|
||||
blocks.total_input_amt AS totalInputAmts
|
||||
blocks.total_input_amt AS totalInputAmt
|
||||
`;
|
||||
|
||||
class BlocksRepository {
|
||||
@@ -171,6 +213,32 @@ class BlocksRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update missing fee amounts fields
|
||||
*
|
||||
* @param blockHash
|
||||
* @param feeAmtPercentiles
|
||||
* @param medianFeeAmt
|
||||
*/
|
||||
public async $updateFeeAmounts(blockHash: string, feeAmtPercentiles, medianFeeAmt) : Promise<void> {
|
||||
try {
|
||||
const query = `
|
||||
UPDATE blocks
|
||||
SET fee_percentiles = ?, median_fee_amt = ?
|
||||
WHERE hash = ?
|
||||
`;
|
||||
const params: any[] = [
|
||||
JSON.stringify(feeAmtPercentiles),
|
||||
medianFeeAmt,
|
||||
blockHash
|
||||
];
|
||||
await DB.query(query, params);
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot update fee amounts for block ${blockHash}. Reason: ' + ${e instanceof Error ? e.message : e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all block height that have not been indexed between [startHeight, endHeight]
|
||||
*/
|
||||
@@ -432,7 +500,7 @@ class BlocksRepository {
|
||||
|
||||
const blocks: BlockExtended[] = [];
|
||||
for (const block of rows) {
|
||||
blocks.push(await this.formatDbBlockIntoExtendedBlock(block));
|
||||
blocks.push(await this.formatDbBlockIntoExtendedBlock(block as DatabaseBlock));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
@@ -459,37 +527,13 @@ class BlocksRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.formatDbBlockIntoExtendedBlock(rows[0]);
|
||||
return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock);
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one block by hash
|
||||
*/
|
||||
public async $getBlockByHash(hash: string): Promise<object | null> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT ${BLOCK_DB_FIELDS}
|
||||
FROM blocks
|
||||
JOIN pools ON blocks.pool_id = pools.id
|
||||
WHERE hash = ?;
|
||||
`;
|
||||
const [rows]: any[] = await DB.query(query, [hash]);
|
||||
|
||||
if (rows.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.formatDbBlockIntoExtendedBlock(rows[0]);
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return blocks difficulty
|
||||
*/
|
||||
@@ -599,7 +643,6 @@ class BlocksRepository {
|
||||
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
|
||||
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}`);
|
||||
await this.$deleteBlocksFrom(blocks[idx - 1].height);
|
||||
await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height);
|
||||
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
|
||||
await DifficultyAdjustmentsRepository.$deleteAdjustementsFromHeight(blocks[idx - 1].height);
|
||||
return false;
|
||||
@@ -619,7 +662,7 @@ class BlocksRepository {
|
||||
* Delete blocks from the database from blockHeight
|
||||
*/
|
||||
public async $deleteBlocksFrom(blockHeight: number) {
|
||||
logger.info(`Delete newer blocks from height ${blockHeight} from the database`);
|
||||
logger.info(`Delete newer blocks from height ${blockHeight} from the database`, logger.tags.mining);
|
||||
|
||||
try {
|
||||
await DB.query(`DELETE FROM blocks where height >= ${blockHeight}`);
|
||||
@@ -933,7 +976,7 @@ class BlocksRepository {
|
||||
*
|
||||
* @param dbBlk
|
||||
*/
|
||||
private async formatDbBlockIntoExtendedBlock(dbBlk: any): Promise<BlockExtended> {
|
||||
private async formatDbBlockIntoExtendedBlock(dbBlk: DatabaseBlock): Promise<BlockExtended> {
|
||||
const blk: Partial<BlockExtended> = {};
|
||||
const extras: Partial<BlockExtension> = {};
|
||||
|
||||
@@ -997,6 +1040,7 @@ class BlocksRepository {
|
||||
}
|
||||
|
||||
// If we're missing block summary related field, check if we can populate them on the fly now
|
||||
// This is for example triggered upon re-org
|
||||
if (Common.blocksSummariesIndexingEnabled() &&
|
||||
(extras.medianFeeAmt === null || extras.feePercentiles === null))
|
||||
{
|
||||
@@ -1004,11 +1048,12 @@ class BlocksRepository {
|
||||
if (extras.feePercentiles === null) {
|
||||
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
|
||||
const summary = blocks.summarizeBlock(block);
|
||||
await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary });
|
||||
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions);
|
||||
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
|
||||
}
|
||||
if (extras.feePercentiles !== null) {
|
||||
extras.medianFeeAmt = extras.feePercentiles[3];
|
||||
await this.$updateFeeAmounts(dbBlk.id, extras.feePercentiles, extras.medianFeeAmt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import { BlockSummary } from '../mempool.interfaces';
|
||||
import { BlockSummary, TransactionStripped } from '../mempool.interfaces';
|
||||
|
||||
class BlocksSummariesRepository {
|
||||
public async $getByBlockId(id: string): Promise<BlockSummary | undefined> {
|
||||
@@ -17,23 +17,17 @@ class BlocksSummariesRepository {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async $saveSummary(params: { height: number, mined?: BlockSummary}) {
|
||||
const blockId = params.mined?.id;
|
||||
public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionStripped[]): Promise<void> {
|
||||
try {
|
||||
const transactions = JSON.stringify(params.mined?.transactions || []);
|
||||
const transactionsStr = JSON.stringify(transactions);
|
||||
await DB.query(`
|
||||
INSERT INTO blocks_summaries (height, id, transactions, template)
|
||||
VALUE (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
transactions = ?
|
||||
`, [params.height, blockId, transactions, '[]', transactions]);
|
||||
INSERT INTO blocks_summaries
|
||||
SET height = ?, transactions = ?, id = ?
|
||||
ON DUPLICATE KEY UPDATE transactions = ?`,
|
||||
[blockHeight, transactionsStr, blockId, transactionsStr]);
|
||||
} catch (e: any) {
|
||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||
logger.debug(`Cannot save block summary for ${blockId} because it has already been indexed, ignoring`);
|
||||
} else {
|
||||
logger.debug(`Cannot save block summary for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
throw e;
|
||||
}
|
||||
logger.debug(`Cannot save block summary transactions for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,19 +62,6 @@ class BlocksSummariesRepository {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete blocks from the database from blockHeight
|
||||
*/
|
||||
public async $deleteBlocksFrom(blockHeight: number) {
|
||||
logger.info(`Delete newer blocks summary from height ${blockHeight} from the database`);
|
||||
|
||||
try {
|
||||
await DB.query(`DELETE FROM blocks_summaries where height >= ${blockHeight}`);
|
||||
} catch (e) {
|
||||
logger.err('Cannot delete indexed blocks summaries. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fee percentiles if the block has already been indexed, [] otherwise
|
||||
*
|
||||
|
||||
@@ -220,7 +220,7 @@ class HashratesRepository {
|
||||
* Delete hashrates from the database from timestamp
|
||||
*/
|
||||
public async $deleteHashratesFromTimestamp(timestamp: number) {
|
||||
logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`);
|
||||
logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`, logger.tags.mining);
|
||||
|
||||
try {
|
||||
await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp >= FROM_UNIXTIME(?)`, [timestamp]);
|
||||
|
||||
@@ -160,7 +160,7 @@ class PricesRepository {
|
||||
|
||||
// Compute fiat exchange rates
|
||||
let latestPrice = rates[0] as ApiPrice;
|
||||
if (latestPrice.USD === -1) {
|
||||
if (!latestPrice || latestPrice.USD === -1) {
|
||||
latestPrice = priceUpdater.getEmptyPricesObj();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class ForensicsService {
|
||||
|
||||
private async $runTasks(): Promise<void> {
|
||||
try {
|
||||
logger.info(`Running forensics scans`);
|
||||
logger.debug(`Running forensics scans`);
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
await this.$runClosedChannelsForensics(false);
|
||||
@@ -73,7 +73,7 @@ class ForensicsService {
|
||||
let progress = 0;
|
||||
|
||||
try {
|
||||
logger.info(`Started running closed channel forensics...`);
|
||||
logger.debug(`Started running closed channel forensics...`);
|
||||
let channels;
|
||||
if (onlyNewChannels) {
|
||||
channels = await channelsApi.$getClosedChannelsWithoutReason();
|
||||
@@ -152,11 +152,11 @@ class ForensicsService {
|
||||
++progress;
|
||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||
if (elapsedSeconds > 10) {
|
||||
logger.info(`Updating channel closed channel forensics ${progress}/${channels.length}`);
|
||||
logger.debug(`Updating channel closed channel forensics ${progress}/${channels.length}`);
|
||||
this.loggerTimer = new Date().getTime() / 1000;
|
||||
}
|
||||
}
|
||||
logger.info(`Closed channels forensics scan complete.`);
|
||||
logger.debug(`Closed channels forensics scan complete.`);
|
||||
} catch (e) {
|
||||
logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@@ -217,7 +217,7 @@ class ForensicsService {
|
||||
let progress = 0;
|
||||
|
||||
try {
|
||||
logger.info(`Started running open channel forensics...`);
|
||||
logger.debug(`Started running open channel forensics...`);
|
||||
const channels = await channelsApi.$getChannelsWithoutSourceChecked();
|
||||
|
||||
for (const openChannel of channels) {
|
||||
@@ -257,7 +257,7 @@ class ForensicsService {
|
||||
++progress;
|
||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||
if (elapsedSeconds > 10) {
|
||||
logger.info(`Updating opened channel forensics ${progress}/${channels?.length}`);
|
||||
logger.debug(`Updating opened channel forensics ${progress}/${channels?.length}`);
|
||||
this.loggerTimer = new Date().getTime() / 1000;
|
||||
this.truncateTempCache();
|
||||
}
|
||||
@@ -266,7 +266,7 @@ class ForensicsService {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Open channels forensics scan complete.`);
|
||||
logger.debug(`Open channels forensics scan complete.`);
|
||||
} catch (e) {
|
||||
logger.err('$runOpenedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
|
||||
} finally {
|
||||
|
||||
@@ -287,7 +287,7 @@ class NetworkSyncService {
|
||||
} else {
|
||||
log += ` for the first time`;
|
||||
}
|
||||
logger.info(`${log}`, logger.tags.ln);
|
||||
logger.debug(`${log}`, logger.tags.ln);
|
||||
|
||||
const channels = await channelsApi.$getChannelsByStatus([0, 1]);
|
||||
for (const channel of channels) {
|
||||
@@ -304,7 +304,7 @@ class NetworkSyncService {
|
||||
++progress;
|
||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||
logger.info(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln);
|
||||
logger.debug(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln);
|
||||
this.loggerTimer = new Date().getTime() / 1000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,16 +15,20 @@ class LightningStatsImporter {
|
||||
topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER;
|
||||
|
||||
async $run(): Promise<void> {
|
||||
const [channels]: any[] = await DB.query('SELECT short_id from channels;');
|
||||
logger.info(`Caching funding txs for currently existing channels`, logger.tags.ln);
|
||||
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
|
||||
try {
|
||||
const [channels]: any[] = await DB.query('SELECT short_id from channels;');
|
||||
logger.info(`Caching funding txs for currently existing channels`, logger.tags.ln);
|
||||
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
|
||||
|
||||
if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
|
||||
return;
|
||||
if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$importHistoricalLightningStats();
|
||||
await this.$cleanupIncorrectSnapshot();
|
||||
} catch (e) {
|
||||
logger.err(`Exception in LightningStatsImporter::$run(). ${e}`);
|
||||
}
|
||||
|
||||
await this.$importHistoricalLightningStats();
|
||||
await this.$cleanupIncorrectSnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,7 +62,7 @@ class PoolsUpdater {
|
||||
if (this.currentSha === null) {
|
||||
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
||||
} else {
|
||||
logger.warn(`pools-v2.json is outdated, fetch latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
||||
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
||||
}
|
||||
const poolsJson = await this.query(this.poolsUrl);
|
||||
if (poolsJson === undefined) {
|
||||
|
||||
@@ -222,7 +222,7 @@ class PriceUpdater {
|
||||
private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise<void> {
|
||||
const existingPriceTimes = await PricesRepository.$getPricesTimes();
|
||||
|
||||
logger.info(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database`, logger.tags.mining);
|
||||
logger.debug(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database`, logger.tags.mining);
|
||||
|
||||
const historicalPrices: PriceHistory[] = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user