Merge pull request #4045 from mempool/mononaut/fast-blocks

Faster blocks
This commit is contained in:
softsimon 2023-07-25 16:07:52 +09:00 committed by GitHub
commit b58abe4779
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 69 additions and 42 deletions

View File

@ -8,6 +8,7 @@ export interface AbstractBitcoinApi {
$getBlockHeightTip(): Promise<number>; $getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>; $getBlockHashTip(): Promise<string>;
$getTxIdsForBlock(hash: string): Promise<string[]>; $getTxIdsForBlock(hash: string): Promise<string[]>;
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]>;
$getBlockHash(height: number): Promise<string>; $getBlockHash(height: number): Promise<string>;
$getBlockHeader(hash: string): Promise<string>; $getBlockHeader(hash: string): Promise<string>;
$getBlock(hash: string): Promise<IEsploraApi.Block>; $getBlock(hash: string): Promise<IEsploraApi.Block>;

View File

@ -81,6 +81,10 @@ class BitcoinApi implements AbstractBitcoinApi {
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
} }
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.');
}
$getRawBlock(hash: string): Promise<Buffer> { $getRawBlock(hash: string): Promise<Buffer> {
return this.bitcoindClient.getBlock(hash, 0) return this.bitcoindClient.getBlock(hash, 0)
.then((raw: string) => Buffer.from(raw, "hex")); .then((raw: string) => Buffer.from(raw, "hex"));

View File

@ -89,6 +89,10 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids'); return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
} }
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txs');
}
$getBlockHash(height: number): Promise<string> { $getBlockHash(height: number): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height); return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
} }

View File

@ -70,6 +70,9 @@ class Blocks {
* @param blockHash * @param blockHash
* @param blockHeight * @param blockHeight
* @param onlyCoinbase - Set to true if you only need the coinbase transaction * @param onlyCoinbase - Set to true if you only need the coinbase transaction
* @param txIds - optional ordered list of transaction ids if already known
* @param quiet - don't print non-essential logs
* @param addMempoolData - calculate sigops etc
* @returns Promise<TransactionExtended[]> * @returns Promise<TransactionExtended[]>
*/ */
private async $getTransactionsExtended( private async $getTransactionsExtended(
@ -80,62 +83,77 @@ class Blocks {
quiet: boolean = false, quiet: boolean = false,
addMempoolData: boolean = false, addMempoolData: boolean = false,
): Promise<TransactionExtended[]> { ): Promise<TransactionExtended[]> {
const transactions: TransactionExtended[] = []; const isEsplora = config.MEMPOOL.BACKEND === 'esplora';
const transactionMap: { [txid: string]: TransactionExtended } = {};
if (!txIds) { if (!txIds) {
txIds = await bitcoinApi.$getTxIdsForBlock(blockHash); txIds = await bitcoinApi.$getTxIdsForBlock(blockHash);
} }
const mempool = memPool.getMempool(); const mempool = memPool.getMempool();
let transactionsFound = 0; let foundInMempool = 0;
let transactionsFetched = 0; let totalFound = 0;
for (let i = 0; i < txIds.length; i++) { // Copy existing transactions from the mempool
if (mempool[txIds[i]]) { if (!onlyCoinbase) {
// We update blocks before the mempool (index.ts), therefore we can for (const txid of txIds) {
// optimize here by directly fetching txs in the "outdated" mempool if (mempool[txid]) {
transactions.push(mempool[txIds[i]]); transactionMap[txid] = mempool[txid];
transactionsFound++; foundInMempool++;
} else if (config.MEMPOOL.BACKEND === 'esplora' || !memPool.hasPriority() || i === 0) { totalFound++;
// Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...)
if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam
logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
}
try {
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData);
transactions.push(tx);
transactionsFetched++;
} catch (e) {
try {
if (config.MEMPOOL.BACKEND === 'esplora') {
// Try again with core
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true, addMempoolData);
transactions.push(tx);
transactionsFetched++;
} else {
throw e;
}
} catch (e) {
if (i === 0) {
const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
throw new Error(msg);
} else {
logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
} }
} }
}
if (onlyCoinbase === true) { // Skip expensive lookups while mempool has priority
break; // Fetch the first transaction and exit if (onlyCoinbase) {
try {
const coinbase = await transactionUtils.$getTransactionExtended(txIds[0], false, false, false, addMempoolData);
return [coinbase];
} catch (e) {
const msg = `Cannot fetch coinbase tx ${txIds[0]}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
throw new Error(msg);
}
}
// Fetch remaining txs in bulk
if (isEsplora && (txIds.length - totalFound > 500)) {
try {
const rawTransactions = await bitcoinApi.$getTxsForBlock(blockHash);
for (const tx of rawTransactions) {
if (!transactionMap[tx.txid]) {
transactionMap[tx.txid] = addMempoolData ? transactionUtils.extendMempoolTransaction(tx) : transactionUtils.extendTransaction(tx);
totalFound++;
}
}
} catch (e) {
logger.err(`Cannot fetch bulk txs for block ${blockHash}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
// Fetch remaining txs individually
for (const txid of txIds.filter(txid => !transactionMap[txid])) {
if (!transactionMap[txid]) {
if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
}
try {
const tx = await transactionUtils.$getTransactionExtended(txid, false, false, false, addMempoolData);
transactionMap[txid] = tx;
totalFound++;
} catch (e) {
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
}
} }
} }
if (!quiet) { if (!quiet) {
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`); logger.debug(`${foundInMempool} of ${txIds.length} found in mempool. ${totalFound - foundInMempool} fetched through backend service.`);
} }
return transactions; // Return list of transactions, preserving block order
return txIds.map(txid => transactionMap[txid]).filter(tx => tx != null);
} }
/** /**

View File

@ -53,7 +53,7 @@ class TransactionUtils {
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended; return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
} }
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended { public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
// @ts-ignore // @ts-ignore
if (transaction.vsize) { if (transaction.vsize) {
// @ts-ignore // @ts-ignore