From 4b8df2978da5368fa46c7702358d779733c214cb Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Thu, 4 Feb 2021 07:22:55 +0200 Subject: [PATCH 01/11] Graceful degradation for pruned and txindex-less nodes `txindex` related changes: - Fetch block transactions with `getrawtransaction `. This works without `txindex`. - Tolerate missing previous output information and render transactions without it. Pruning related changes: - Fallback to `getblockheader` instead of `getblock` for pruned blocks. The txid list and weight/size information will be missing. - Tolerate missing block stats. --- app/api/coreApi.js | 38 ++++++++++++-------- app/api/rpcApi.js | 36 +++++++++++-------- app/utils.js | 30 +++++++++------- routes/apiRouter.js | 4 +-- routes/baseRouter.js | 15 ++++---- views/block-analysis.pug | 4 +-- views/includes/block-content.pug | 42 +++++++++++++---------- views/includes/blocks-list.pug | 26 +++++++++----- views/includes/transaction-io-details.pug | 27 +++++++++------ views/mining-summary.pug | 4 +-- 10 files changed, 132 insertions(+), 94 deletions(-) diff --git a/app/api/coreApi.js b/app/api/coreApi.js index c331ee157..2a50439e6 100644 --- a/app/api/coreApi.js +++ b/app/api/coreApi.js @@ -626,9 +626,9 @@ function getBlocksByHash(blockHashes) { }); } -function getRawTransaction(txid) { +function getRawTransaction(txid, blockhash) { var rpcApiFunction = function() { - return rpcApi.getRawTransaction(txid); + return rpcApi.getRawTransaction(txid, blockhash); }; return tryCacheThenRpcApi(txCache, "getRawTransaction-" + txid, ONE_HR, rpcApiFunction, shouldCacheTransaction); @@ -720,11 +720,11 @@ function getAddress(address) { }); } -function getRawTransactions(txids) { +function getRawTransactions(txids, blockhash) { return new Promise(function(resolve, reject) { var promises = []; for (var i = 0; i < txids.length; i++) { - promises.push(getRawTransaction(txids[i])); + promises.push(getRawTransaction(txids[i], blockhash)); } Promise.all(promises).then(function(results) { @@ -828,9 +828,9 @@ function summarizeBlockAnalysisData(blockHeight, tx, inputs) { return txSummary; } -function getRawTransactionsWithInputs(txids, maxInputs=-1) { +function getRawTransactionsWithInputs(txids, maxInputs=-1, blockhash) { return new Promise(function(resolve, reject) { - getRawTransactions(txids).then(function(transactions) { + getRawTransactions(txids, blockhash).then(function(transactions) { var maxInputsTracked = config.site.txMaxInput; if (maxInputs <= 0) { @@ -885,10 +885,12 @@ function getRawTransactionsWithInputs(txids, maxInputs=-1) { }); resolve({ transactions:transactions, txInputsByTransaction:txInputsByTransaction }); + }).catch(function (err) { + debugLog("fetching prevouts failed:", err); + // likely due to pruning or no txindex, report the error but continue with an empty inputs map + resolve({ transactions:transactions, txInputsByTransaction: {} }); }); - }).catch(function(err) { - reject(err); - }); + }).catch(reject); }); } @@ -905,7 +907,7 @@ function getBlockByHashWithTransactions(blockHash, txLimit, txOffset) { txids.push(block.tx[i]); } - getRawTransactionsWithInputs(txids, config.site.txMaxInput).then(function(txsResult) { + getRawTransactionsWithInputs(txids, config.site.txMaxInput, blockHash).then(function(txsResult) { if (txsResult.transactions && txsResult.transactions.length > 0) { block.coinbaseTx = txsResult.transactions[0]; block.totalFees = utils.getBlockTotalFeesFromCoinbaseTxAndBlockHeight(block.coinbaseTx, block.height); @@ -918,13 +920,18 @@ function getBlockByHashWithTransactions(blockHash, txLimit, txOffset) { } resolve({ getblock:block, transactions:txsResult.transactions, txInputsByTransaction:txsResult.txInputsByTransaction }); + }).catch(function(err) { + // likely due to pruning or no txindex, report the error but continue with an empty transaction list + resolve({ getblock:block, transactions:[], txInputsByTransaction:{} }); }); - }).catch(function(err) { - reject(err); - }); + }).catch(reject); }); } +function getTxOut(txid, vout) { + return rpcApi.getTxOut(txid, vout) +} + function getHelp() { return new Promise(function(resolve, reject) { tryCacheThenRpcApi(miscCache, "getHelp", ONE_DAY, rpcApi.getHelp).then(function(helpContent) { @@ -1084,5 +1091,6 @@ module.exports = { getBlocksStatsByHeight: getBlocksStatsByHeight, buildBlockAnalysisData: buildBlockAnalysisData, getBlockHeaderByHeight: getBlockHeaderByHeight, - getBlockHeadersByHeight: getBlockHeadersByHeight -}; \ No newline at end of file + getBlockHeadersByHeight: getBlockHeadersByHeight, + getTxOut: getTxOut +}; diff --git a/app/api/rpcApi.js b/app/api/rpcApi.js index bd45303d1..c214d6c2f 100644 --- a/app/api/rpcApi.js +++ b/app/api/rpcApi.js @@ -192,30 +192,30 @@ function getBlockHeaderByHeight(blockHeight) { function getBlockByHash(blockHash) { debugLog("getBlockByHash: %s", blockHash); - return new Promise(function(resolve, reject) { - getRpcDataWithParams({method:"getblock", parameters:[blockHash]}).then(function(block) { - getRawTransaction(block.tx[0]).then(function(tx) { + return getRpcDataWithParams({method:"getblock", parameters:[blockHash]}) + .then(function(block) { + return getRawTransaction(block.tx[0], blockHash).then(function(tx) { block.coinbaseTx = tx; block.totalFees = utils.getBlockTotalFeesFromCoinbaseTxAndBlockHeight(tx, block.height); - block.subsidy = coinConfig.blockRewardFunction(block.height, global.activeBlockchain); block.miner = utils.getMinerFromCoinbaseTx(tx); - - resolve(block); - - }).catch(function(err) { - reject(err); - }); + return block; + }) }).catch(function(err) { - reject(err); - }); - }); + // the block is pruned, use `getblockheader` instead + debugLog('getblock failed, falling back to getblockheader', blockHash, err); + return getRpcDataWithParams({method:"getblockheader", parameters:[blockHash]}) + .then(function(block) { block.tx = []; return block }); + }).then(function(block) { + block.subsidy = coinConfig.blockRewardFunction(block.height, global.activeBlockchain); + return block; + }) } function getAddress(address) { return getRpcDataWithParams({method:"validateaddress", parameters:[address]}); } -function getRawTransaction(txid) { +function getRawTransaction(txid, blockhash) { debugLog("getRawTransaction: %s", txid); return new Promise(function(resolve, reject) { @@ -238,7 +238,8 @@ function getRawTransaction(txid) { }); } else { - getRpcDataWithParams({method:"getrawtransaction", parameters:[txid, 1]}).then(function(result) { + var extra_params = blockhash ? [ blockhash ] : []; + getRpcDataWithParams({method:"getrawtransaction", parameters:[txid, 1, ...extra_params]}).then(function(result) { if (result == null || result.code && result.code < 0) { reject(result); @@ -331,6 +332,10 @@ function getMempoolTxDetails(txid, includeAncDec=true) { }); } +function getTxOut(txid, vout) { + return getRpcDataWithParams({method:"gettxout", parameters:[txid, vout]}); +} + function getHelp() { return getRpcData("help"); } @@ -502,6 +507,7 @@ module.exports = { getBlockStatsByHeight: getBlockStatsByHeight, getBlockHeaderByHash: getBlockHeaderByHash, getBlockHeaderByHeight: getBlockHeaderByHeight, + getTxOut: getTxOut, minRpcVersions: minRpcVersions }; \ No newline at end of file diff --git a/app/utils.js b/app/utils.js index 2c66dc4a9..4f7599f98 100644 --- a/app/utils.js +++ b/app/utils.js @@ -451,26 +451,30 @@ function getTxTotalInputOutputValues(tx, txInputs, blockHeight) { var totalOutputValue = new Decimal(0); try { - for (var i = 0; i < tx.vin.length; i++) { - if (tx.vin[i].coinbase) { - totalInputValue = totalInputValue.plus(new Decimal(coinConfig.blockRewardFunction(blockHeight, global.activeBlockchain))); + if (txInputs) { + for (var i = 0; i < tx.vin.length; i++) { + if (tx.vin[i].coinbase) { + totalInputValue = totalInputValue.plus(new Decimal(coinConfig.blockRewardFunction(blockHeight, global.activeBlockchain))); - } else { - var txInput = txInputs[i]; + } else { + var txInput = txInputs[i]; - if (txInput) { - try { - var vout = txInput; - if (vout.value) { - totalInputValue = totalInputValue.plus(new Decimal(vout.value)); + if (txInput) { + try { + var vout = txInput; + if (vout.value) { + totalInputValue = totalInputValue.plus(new Decimal(vout.value)); + } + } catch (err) { + logError("2397gs0gsse", err, {txid:tx.txid, vinIndex:i}); } - } catch (err) { - logError("2397gs0gsse", err, {txid:tx.txid, vinIndex:i}); } } } + } else { + totalInputValue = null } - + for (var i = 0; i < tx.vout.length; i++) { totalOutputValue = totalOutputValue.plus(new Decimal(tx.vout[i].value)); } diff --git a/routes/apiRouter.js b/routes/apiRouter.js index 0bd5e09ab..3c6086244 100644 --- a/routes/apiRouter.js +++ b/routes/apiRouter.js @@ -35,9 +35,7 @@ router.get("/blocks-by-height/:blockHeights", function(req, res, next) { coreApi.getBlocksByHeight(blockHeights).then(function(result) { res.json(result); - - next(); - }); + }).catch(next); }); router.get("/block-headers-by-height/:blockHeights", function(req, res, next) { diff --git a/routes/baseRouter.js b/routes/baseRouter.js index d4192b7fb..59309f2ec 100644 --- a/routes/baseRouter.js +++ b/routes/baseRouter.js @@ -435,7 +435,7 @@ router.get("/blocks", function(req, res, next) { promises.push(coreApi.getBlocksByHeight(blockHeights)); - promises.push(coreApi.getBlocksStatsByHeight(blockHeights)); + promises.push(coreApi.getBlocksStatsByHeight(blockHeights).catch(_ => ({}))); Promise.all(promises).then(function(promiseResults) { res.locals.blocks = promiseResults[0]; @@ -665,9 +665,10 @@ router.get("/block-height/:blockHeight", function(req, res, next) { resolve(); }).catch(function(err) { - res.locals.pageErrors.push(utils.logError("983yr435r76d", err)); - - reject(err); + // unavailable, likely due to pruning + debugLog('Failed loading block stats', err) + res.locals.result.blockstats = null; + resolve(); }); })); @@ -747,9 +748,9 @@ router.get("/block/:blockHash", function(req, res, next) { resolve(); }).catch(function(err) { - res.locals.pageErrors.push(utils.logError("21983ue8hye", err)); - - reject(err); + // unavailable, likely due to pruning + debugLog('Failed loading block stats', err) + resolve(); }); })); diff --git a/views/block-analysis.pug b/views/block-analysis.pug index f0376720c..3528491eb 100644 --- a/views/block-analysis.pug +++ b/views/block-analysis.pug @@ -47,7 +47,7 @@ block content span.text-muted (#{new Decimal(100 * result.getblock.weight / coinConfig.maxBlockWeight).toDecimalPlaces(2)}% full) - else + else if (result.getblock.size) div.row div.summary-table-label Size div.summary-table-content.text-monospace #{result.getblock.size.toLocaleString()} @@ -55,7 +55,7 @@ block content div.row div.summary-table-label Transactions - div.summary-table-content.text-monospace #{result.getblock.tx.length.toLocaleString()} + div.summary-table-content.text-monospace #{result.getblock.nTx.toLocaleString()} div.row div.summary-table-label Confirmations diff --git a/views/includes/block-content.pug b/views/includes/block-content.pug index 3e9bb7e85..d731954c5 100644 --- a/views/includes/block-content.pug +++ b/views/includes/block-content.pug @@ -22,7 +22,7 @@ ul.nav.nav-tabs.mb-3 li.nav-item a.nav-link(data-toggle="tab", href="#tab-json", role="tab") JSON -- var txCount = result.getblock.tx.length; +- var txCount = result.getblock.nTx div.tab-content div.tab-pane.active(id="tab-details", role="tabpanel") @@ -140,18 +140,19 @@ div.tab-content else span 0 - - var blockRewardMax = coinConfig.blockRewardFunction(result.getblock.height, activeBlockchain); - - var coinbaseTxTotalOutputValue = new Decimal(0); - each vout in result.getblock.coinbaseTx.vout - - coinbaseTxTotalOutputValue = coinbaseTxTotalOutputValue.plus(new Decimal(vout.value)); + if result.getblock.coinbaseTx + - var blockRewardMax = coinConfig.blockRewardFunction(result.getblock.height, activeBlockchain); + - var coinbaseTxTotalOutputValue = new Decimal(0); + each vout in result.getblock.coinbaseTx.vout + - coinbaseTxTotalOutputValue = coinbaseTxTotalOutputValue.plus(new Decimal(vout.value)); - if (parseFloat(coinbaseTxTotalOutputValue) < blockRewardMax) - div.row - div(class=sumTableLabelClass) - span.border-dotted(data-toggle="tooltip" title="The miner of this block failed to collect this value. As a result, it is permanently lost.") Value Destroyed - div.text-monospace.text-danger(class=sumTableValueClass) - - var currencyValue = new Decimal(blockRewardMax).minus(coinbaseTxTotalOutputValue); - include ./value-display.pug + if (parseFloat(coinbaseTxTotalOutputValue) < blockRewardMax) + div.row + div(class=sumTableLabelClass) + span.border-dotted(data-toggle="tooltip" title="The miner of this block failed to collect this value. As a result, it is permanently lost.") Value Destroyed + div.text-monospace.text-danger(class=sumTableValueClass) + - var currencyValue = new Decimal(blockRewardMax).minus(coinbaseTxTotalOutputValue); + include ./value-display.pug if (result.getblock.weight) div.row @@ -163,10 +164,12 @@ div.tab-content span.text-muted (#{new Decimal(100 * result.getblock.weight / coinConfig.maxBlockWeight).toDecimalPlaces(2)}% full) - div.row - div(class=sumTableLabelClass) Size - div.text-monospace(class=sumTableValueClass) #{result.getblock.size.toLocaleString()} - small B + + if (result.getblock.size) + div.row + div(class=sumTableLabelClass) Size + div.text-monospace(class=sumTableValueClass) #{result.getblock.size.toLocaleString()} + small B if (result.getblock.miner) div.row @@ -348,8 +351,11 @@ div.tab-content span.text-muted (#{result.getblock.chainwork.replace(/^0+/, '')}) - div.card.shadow-sm.mb-3 - div.card-body.px-2.px-md-3 + if (!result.getblock.tx.length) + div.card.shadow-sm.mb-3: div.card-body.px-2.px-md-3 + h2.h6.mb-0.d-inline-block #{txCount.toLocaleString()} Transaction#{txCount>1?'s':''} (unavailable due to pruning) + else + div.card.shadow-sm.mb-3: div.card-body.px-2.px-md-3 div.row div.col-md-4 h2.h6.mb-0.d-inline-block #{txCount.toLocaleString()} diff --git a/views/includes/blocks-list.pug b/views/includes/blocks-list.pug index d90a8a5da..73c42efbd 100644 --- a/views/includes/blocks-list.pug +++ b/views/includes/blocks-list.pug @@ -110,7 +110,7 @@ div.table-responsive span ? - td.data-cell.text-monospace.text-right #{block.tx.length.toLocaleString()} + td.data-cell.text-monospace.text-right #{block.nTx.toLocaleString()} if (blockstatsByHeight) td.data-cell.text-monospace.text-right @@ -119,22 +119,32 @@ div.table-responsive - var currencyValueDecimals = 0; include ./value-display.pug + if (block.totalFees == null) + - block.totalFees = Decimal(blockstatsByHeight[block.height].totalfee).dividedBy(coinConfig.baseCurrencyUnit.multiplier) + else span 0 td.data-cell.text-monospace.text-right - - var currencyValue = new Decimal(block.totalFees).dividedBy(block.strippedsize).times(coinConfig.baseCurrencyUnit.multiplier).toDecimalPlaces(1); - span #{currencyValue} - + if block.totalFees && block.strippedsize + - var currencyValue = new Decimal(block.totalFees).dividedBy(block.strippedsize).times(coinConfig.baseCurrencyUnit.multiplier).toDecimalPlaces(1); + span #{currencyValue} + else + = 'N/A' + // idea: show typical tx fee, maybe also optimized fee if not segwit if (false) - var feeEstimateVal = currencyValue.times(166).dividedBy(coinConfig.baseCurrencyUnit.multiplier); span.border-dotted(title=`Value: ${feeEstimateVal}`, data-toggle="tooltip") #{currencyValue} td.data-cell.text-monospace.text-right - - var currencyValue = new Decimal(block.totalFees); - - var currencyValueDecimals = 3; - include ./value-display.pug + if block.totalFees + - var currencyValue = new Decimal(block.totalFees); + - var currencyValueDecimals = 3; + include ./value-display.pug + else + = 'N/A' + @@ -144,7 +154,7 @@ div.table-responsive span #{bSizeK.toLocaleString()} if (blocks && blocks.length > 0 && blocks[0].weight) - td.data-cell.text-monospace.text-right + td.data-cell.text-monospace.text-right: if block.weight if (true) - var full = new Decimal(block.weight).dividedBy(coinConfig.maxBlockWeight).times(100); - var full2 = full.toDP(2); diff --git a/views/includes/transaction-io-details.pug b/views/includes/transaction-io-details.pug index 6223e9715..4b02b2e0f 100644 --- a/views/includes/transaction-io-details.pug +++ b/views/includes/transaction-io-details.pug @@ -15,7 +15,7 @@ script. div.row.text-monospace div.col-lg-6 - if (txInputs) + if (true) - var extraInputCount = 0; each txVin, txVinIndex in tx.vin if (!txVin.coinbase) @@ -23,8 +23,11 @@ div.row.text-monospace if (txInputs && txInputs[txVinIndex]) - var txInput = txInputs[txVinIndex]; - var vout = txInput; + else + - var txInput = {txid:txVin.txid, vout:txVin.vout}; + - var vout = {}; - if (txVin.coinbase || vout) + if true div.clearfix div.tx-io-label a(data-toggle="tooltip", title=("Input #" + txVinIndex.toLocaleString()), style="white-space: nowrap;") @@ -55,14 +58,15 @@ div.row.text-monospace div(id="coinbase-data-hex", style="display: none;") small.font-weight-bold data - small (hex) - + small (hex) - small.word-wrap #{txVin.coinbase} else div.word-wrap - small.data-tag.bg-dark.mr-2 - span(title=`Input Type: ${utils.outputTypeName(vout.scriptPubKey.type)}`, data-toggle="tooltip") #{utils.outputTypeAbbreviation(vout.scriptPubKey.type)} + if (vout.scriptPubKey) + small.data-tag.bg-dark.mr-2 + span(title=`Input Type: ${utils.outputTypeName(vout.scriptPubKey.type)}`, data-toggle="tooltip") #{utils.outputTypeAbbreviation(vout.scriptPubKey.type)} if (vout.coinbaseSpend) small.data-tag.bg-success.mr-2 @@ -143,12 +147,13 @@ div.row.text-monospace include ./value-display.pug hr - div.row.mb-5.mb-lg-0 - div.col - div.font-weight-bold.text-left.text-md-right - span.d-block.d-md-none Total Input: - - var currencyValue = totalIOValues.input; - include ./value-display.pug + if (totalIOValues.input) + div.row.mb-5.mb-lg-0 + div.col + div.font-weight-bold.text-left.text-md-right + span.d-block.d-md-none Total Input: + - var currencyValue = totalIOValues.input; + include ./value-display.pug diff --git a/views/mining-summary.pug b/views/mining-summary.pug index 1dca3448f..3b14e6448 100644 --- a/views/mining-summary.pug +++ b/views/mining-summary.pug @@ -348,7 +348,7 @@ block endOfBody summariesByMiner[miner.name].blockCount++; summariesByMiner[miner.name].transactionCount += block.tx.length; summariesByMiner[miner.name].totalSubsidiesCollected = summariesByMiner[miner.name].totalSubsidiesCollected.add(new Decimal(block.subsidy)); - summariesByMiner[miner.name].totalFeesCollected = summariesByMiner[miner.name].totalFeesCollected.add(new Decimal(block.totalFees)); + if (block.totalFees != null) summariesByMiner[miner.name].totalFeesCollected = summariesByMiner[miner.name].totalFeesCollected.add(new Decimal(block.totalFees)); summariesByMiner[miner.name].blockSizeTotal += block.size; summariesByMiner[miner.name].blockHeights.push(block.height); @@ -361,7 +361,7 @@ block endOfBody overallSummary.blockCount++; overallSummary.transactionCount += block.tx.length; overallSummary.totalSubsidiesCollected = overallSummary.totalSubsidiesCollected.add(new Decimal(block.subsidy)); - overallSummary.totalFeesCollected = overallSummary.totalFeesCollected.add(new Decimal(block.totalFees)); + if (block.totalFees != null) overallSummary.totalFeesCollected = overallSummary.totalFeesCollected.add(new Decimal(block.totalFees)); overallSummary.blockSizeTotal += block.size; if (blocks[0].weight) { From 323b7aa840fbf24473e5230cd5a5f6bf88b49118 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Thu, 4 Feb 2021 07:41:41 +0200 Subject: [PATCH 02/11] Support whitepaper extraction with pruned nodes --- routes/baseRouter.js | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/routes/baseRouter.js b/routes/baseRouter.js index 59309f2ec..6c3b09706 100644 --- a/routes/baseRouter.js +++ b/routes/baseRouter.js @@ -1607,37 +1607,24 @@ router.get("/bitcoin.pdf", function(req, res, next) { // ref: https://bitcoin.stackexchange.com/questions/35959/how-is-the-whitepaper-decoded-from-the-blockchain-tx-with-1000x-m-of-n-multisi const whitepaperTxid = "54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713"; - coreApi.getRawTransaction(whitepaperTxid).then(function(tx) { - var pdfData = ""; - var start; - - // all outputs, except last 3, are 1-of-3 multisigs - for (var i = 0; i < tx.vout.length - 3; i++) { - var parts = tx.vout[i].scriptPubKey.asm.split(" "); - - pdfData += parts[1]; - pdfData += parts[2]; - pdfData += parts[3]; - } - - // the last bit of pdf data is in 3rd-from-last output, a 1-of-1 multisig (last 2 outputs are unused for pdf data) - var parts = tx.vout[tx.vout.length - 3].scriptPubKey.asm.split(" "); - - // last bit is zeroes and is excluded - pdfData += parts[1].substring(0, 50); - - // strip size and checksum from start - pdfData = pdfData.substring(16); + // get all outputs except the last 2 using `gettxout` + Promise.all([...Array(946).keys()].map(vout => coreApi.getTxOut(whitepaperTxid, vout))) + .then(function (vouts) { + // concatenate all multisig pubkeys + var pdfData = vouts.map((out, n) => { + var parts = out.scriptPubKey.asm.split(" ") + // the last output is a 1-of-1 + return n == 945 ? parts[1] : parts.slice(1,4).join('') + }).join('') + + // strip size and checksum from start and null bytes at the end + pdfData = pdfData.slice(16).slice(0, -16); const hexArray = utils.arrayFromHexString(pdfData); - res.contentType("application/pdf"); res.send(Buffer.alloc(hexArray.length, hexArray, "hex")); - - next(); - }).catch(function(err) { - res.locals.userMessageMarkdown = `Failed to load transaction: txid=**${whitepaperTxid}**`; + res.locals.userMessageMarkdown = `Failed to load transaction outputs: txid=**${whitepaperTxid}**`; res.locals.pageErrors.push(utils.logError("432907twhgeyedg", err)); From 5c66c3c16790fc7bd7744320dede9bc6dd213832 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Thu, 4 Feb 2021 08:38:48 +0200 Subject: [PATCH 03/11] Don't attempt to retrieve transactions without the blockhash, unless txindex is available --- app.js | 22 +++++++++++----------- app/api/coreApi.js | 5 +++++ app/api/rpcApi.js | 9 +++++++++ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/app.js b/app.js index 697203f87..338de57e9 100755 --- a/app.js +++ b/app.js @@ -193,29 +193,29 @@ function verifyRpcConnection() { if (!global.activeBlockchain) { debugLog(`Verifying RPC connection...`); - coreApi.getNetworkInfo().then(function(getnetworkinfo) { - coreApi.getBlockchainInfo().then(function(getblockchaininfo) { - global.activeBlockchain = getblockchaininfo.chain; + Promise.all([ + coreApi.getNetworkInfo(), + coreApi.getBlockchainInfo(), + coreApi.getIndexInfo(), + ]).then(([ getnetworkinfo, getblockchaininfo, getindexinfo ]) => { + global.activeBlockchain = getblockchaininfo.chain; - // we've verified rpc connection, no need to keep trying - clearInterval(global.verifyRpcConnectionIntervalId); + // we've verified rpc connection, no need to keep trying + clearInterval(global.verifyRpcConnectionIntervalId); - onRpcConnectionVerified(getnetworkinfo, getblockchaininfo); - - }).catch(function(err) { - utils.logError("329u0wsdgewg6ed", err); - }); + onRpcConnectionVerified(getnetworkinfo, getblockchaininfo, getindexinfo); }).catch(function(err) { utils.logError("32ugegdfsde", err); }); } } -function onRpcConnectionVerified(getnetworkinfo, getblockchaininfo) { +function onRpcConnectionVerified(getnetworkinfo, getblockchaininfo, getindexinfo) { // localservicenames introduced in 0.19 var services = getnetworkinfo.localservicesnames ? ("[" + getnetworkinfo.localservicesnames.join(", ") + "]") : getnetworkinfo.localservices; global.getnetworkinfo = getnetworkinfo; + global.getindexinfo = getindexinfo; var bitcoinCoreVersionRegex = /^.*\/Satoshi\:(.*)\/.*$/; diff --git a/app/api/coreApi.js b/app/api/coreApi.js index 2a50439e6..62b776771 100644 --- a/app/api/coreApi.js +++ b/app/api/coreApi.js @@ -228,6 +228,10 @@ function getMempoolInfo() { return tryCacheThenRpcApi(miscCache, "getMempoolInfo", 5 * ONE_SEC, rpcApi.getMempoolInfo); } +function getIndexInfo() { + return tryCacheThenRpcApi(miscCache, "getIndexInfo", 10 * ONE_SEC, rpcApi.getIndexInfo); +} + function getMempoolTxids() { // no caching, that would be dumb return rpcApi.getMempoolTxids(); @@ -1063,6 +1067,7 @@ module.exports = { getMempoolInfo: getMempoolInfo, getMempoolTxids: getMempoolTxids, getMiningInfo: getMiningInfo, + getIndexInfo: getIndexInfo, getBlockByHeight: getBlockByHeight, getBlocksByHeight: getBlocksByHeight, getBlockByHash: getBlockByHash, diff --git a/app/api/rpcApi.js b/app/api/rpcApi.js index c214d6c2f..2c36c73d1 100644 --- a/app/api/rpcApi.js +++ b/app/api/rpcApi.js @@ -50,6 +50,10 @@ function getMiningInfo() { return getRpcData("getmininginfo"); } +function getIndexInfo() { + return getRpcData("getindexinfo"); +} + function getUptimeSeconds() { return getRpcData("uptime"); } @@ -218,6 +222,10 @@ function getAddress(address) { function getRawTransaction(txid, blockhash) { debugLog("getRawTransaction: %s", txid); + if (global.getindexinfo && !global.getindexinfo.txindex && !blockhash) { + return Promise.reject(new Error('Cannot fetch tx without blockhash, txindex is off')) + } + return new Promise(function(resolve, reject) { if (coins[config.coin].genesisCoinbaseTransactionIdsByNetwork[global.activeBlockchain] && txid == coins[config.coin].genesisCoinbaseTransactionIdsByNetwork[global.activeBlockchain]) { // copy the "confirmations" field from genesis block to the genesis-coinbase tx @@ -488,6 +496,7 @@ module.exports = { getMempoolInfo: getMempoolInfo, getMempoolTxids: getMempoolTxids, getMiningInfo: getMiningInfo, + getIndexInfo: getIndexInfo, getBlockByHeight: getBlockByHeight, getBlockByHash: getBlockByHash, getRawTransaction: getRawTransaction, From ddda7fed2795828664ff62a40a70e776b34b1830 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Thu, 4 Feb 2021 08:44:30 +0200 Subject: [PATCH 04/11] Allow explicitly specifying the transaction height i.e. `/tx/?height=`. This can work without txindex. When txindex is disabled, the block page links to the transactions with this parameter set. --- app/api/coreApi.js | 7 +++++++ app/api/rpcApi.js | 5 +++++ routes/baseRouter.js | 9 +++++++-- views/includes/block-content.pug | 3 ++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/api/coreApi.js b/app/api/coreApi.js index 62b776771..a8ddda0b4 100644 --- a/app/api/coreApi.js +++ b/app/api/coreApi.js @@ -548,6 +548,12 @@ function getBlockByHeight(blockHeight) { }); } +function getBlockHashByHeight(blockHeight) { + return tryCacheThenRpcApi(blockCache, "getBlockHashByHeight-" + blockHeight, ONE_HR, function() { + return rpcApi.getBlockHashByHeight(blockHeight); + }); +} + function getBlocksByHeight(blockHeights) { return new Promise(function(resolve, reject) { var promises = []; @@ -1069,6 +1075,7 @@ module.exports = { getMiningInfo: getMiningInfo, getIndexInfo: getIndexInfo, getBlockByHeight: getBlockByHeight, + getBlockHashByHeight: getBlockHashByHeight, getBlocksByHeight: getBlocksByHeight, getBlockByHash: getBlockByHash, getBlocksByHash: getBlocksByHash, diff --git a/app/api/rpcApi.js b/app/api/rpcApi.js index 2c36c73d1..1c0769166 100644 --- a/app/api/rpcApi.js +++ b/app/api/rpcApi.js @@ -193,6 +193,10 @@ function getBlockHeaderByHeight(blockHeight) { }); } +function getBlockHashByHeight(blockHeight) { + return getRpcDataWithParams({method:"getblockhash", parameters:[blockHeight]}); +} + function getBlockByHash(blockHash) { debugLog("getBlockByHash: %s", blockHash); @@ -516,6 +520,7 @@ module.exports = { getBlockStatsByHeight: getBlockStatsByHeight, getBlockHeaderByHash: getBlockHeaderByHash, getBlockHeaderByHeight: getBlockHeaderByHeight, + getBlockHashByHeight: getBlockHashByHeight, getTxOut: getTxOut, minRpcVersions: minRpcVersions diff --git a/routes/baseRouter.js b/routes/baseRouter.js index 6c3b09706..559b71c39 100644 --- a/routes/baseRouter.js +++ b/routes/baseRouter.js @@ -828,11 +828,16 @@ router.get("/tx/:transactionId", function(req, res, next) { res.locals.result = {}; - coreApi.getRawTransactionsWithInputs([txid]).then(function(rawTxResult) { + var txPromise = req.query.height + ? coreApi.getBlockHashByHeight(parseInt(req.query.height)) + .then(blockhash => coreApi.getRawTransactionsWithInputs([txid], -1, blockhash)) + : coreApi.getRawTransactionsWithInputs([txid], -1); + + txPromise.then(function(rawTxResult) { var tx = rawTxResult.transactions[0]; res.locals.result.getrawtransaction = tx; - res.locals.result.txInputs = rawTxResult.txInputsByTransaction[txid]; + res.locals.result.txInputs = rawTxResult.txInputsByTransaction[txid] || {}; var promises = []; diff --git a/views/includes/block-content.pug b/views/includes/block-content.pug index d731954c5..fa9907ae9 100644 --- a/views/includes/block-content.pug +++ b/views/includes/block-content.pug @@ -386,6 +386,7 @@ div.tab-content - var fontawesomeOutputName = "sign-out-alt"; div + - query_height = global.getindexinfo && !global.getindexinfo.txindex ? `?height=${result.getblock.height}` : '' each tx, txIndex in result.transactions //pre // code.json.bg-light #{JSON.stringify(tx, null, 4)} @@ -394,7 +395,7 @@ div.tab-content if (tx && tx.txid) span(title=`Index in Block: #${(txIndex + offset).toLocaleString()}`, data-toggle="tooltip") ##{(txIndex + offset).toLocaleString()} span – - a(href=("./tx/" + tx.txid)) #{tx.txid} + a(href=("./tx/" + tx.txid + query_height)) #{tx.txid} if (global.specialTransactions && global.specialTransactions[tx.txid]) span From 445fb062a6e4574031a49659d7a35862cb2a025b Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Sun, 14 Feb 2021 05:50:18 +0200 Subject: [PATCH 05/11] Try loading transactions without a blockhash, in case they're in the mempool But still avoid unnecessarily loading prevouts --- app/api/coreApi.js | 12 +++++++----- app/api/rpcApi.js | 4 ---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/api/coreApi.js b/app/api/coreApi.js index a8ddda0b4..1c12709d5 100644 --- a/app/api/coreApi.js +++ b/app/api/coreApi.js @@ -839,6 +839,12 @@ function summarizeBlockAnalysisData(blockHeight, tx, inputs) { } function getRawTransactionsWithInputs(txids, maxInputs=-1, blockhash) { + // Get just the transactions without their prevouts when txindex is disabled + if (global.getindexinfo && !global.getindexinfo.txindex) { + return getRawTransactions(txids, blockhash) + .then(transactions => ({ transactions, txInputsByTransaction: {} })) + } + return new Promise(function(resolve, reject) { getRawTransactions(txids, blockhash).then(function(transactions) { var maxInputsTracked = config.site.txMaxInput; @@ -895,11 +901,7 @@ function getRawTransactionsWithInputs(txids, maxInputs=-1, blockhash) { }); resolve({ transactions:transactions, txInputsByTransaction:txInputsByTransaction }); - }).catch(function (err) { - debugLog("fetching prevouts failed:", err); - // likely due to pruning or no txindex, report the error but continue with an empty inputs map - resolve({ transactions:transactions, txInputsByTransaction: {} }); - }); + }).catch(reject); }).catch(reject); }); } diff --git a/app/api/rpcApi.js b/app/api/rpcApi.js index 1c0769166..e19e92698 100644 --- a/app/api/rpcApi.js +++ b/app/api/rpcApi.js @@ -226,10 +226,6 @@ function getAddress(address) { function getRawTransaction(txid, blockhash) { debugLog("getRawTransaction: %s", txid); - if (global.getindexinfo && !global.getindexinfo.txindex && !blockhash) { - return Promise.reject(new Error('Cannot fetch tx without blockhash, txindex is off')) - } - return new Promise(function(resolve, reject) { if (coins[config.coin].genesisCoinbaseTransactionIdsByNetwork[global.activeBlockchain] && txid == coins[config.coin].genesisCoinbaseTransactionIdsByNetwork[global.activeBlockchain]) { // copy the "confirmations" field from genesis block to the genesis-coinbase tx From 66410a79030b0fa77545bb2f1dcb3e6cd1168b32 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Sun, 14 Feb 2021 06:23:54 +0200 Subject: [PATCH 06/11] Support txid@height searches Also: - Use nicer-looking /tx/@ URLs - Simplify by removing some unnecessary code - Fix a bug where searching for invalid block height hanged --- routes/baseRouter.js | 62 ++++++++------------------------ views/includes/block-content.pug | 6 ++-- 2 files changed, 18 insertions(+), 50 deletions(-) diff --git a/routes/baseRouter.js b/routes/baseRouter.js index 559b71c39..6b7c4b61d 100644 --- a/routes/baseRouter.js +++ b/routes/baseRouter.js @@ -530,51 +530,17 @@ router.post("/search", function(req, res, next) { req.session.query = req.body.query; + // Support txid@height lookups + if (/^[a-f0-9]{64}@\d+$/.test(query)) { + return res.redirect("./tx/" + query); + } + if (query.length == 64) { coreApi.getRawTransaction(query).then(function(tx) { - if (tx) { - res.redirect("./tx/" + query); - - return; - } - - coreApi.getBlockByHash(query).then(function(blockByHash) { - if (blockByHash) { - res.redirect("./block/" + query); - - return; - } - - coreApi.getAddress(rawCaseQuery).then(function(validateaddress) { - if (validateaddress && validateaddress.isvalid) { - res.redirect("./address/" + rawCaseQuery); - - return; - } - }); - - req.session.userMessage = "No results found for query: " + query; - - res.redirect("./"); - - }).catch(function(err) { - req.session.userMessage = "No results found for query: " + query; - - res.redirect("./"); - }); - + res.redirect("./tx/" + query); }).catch(function(err) { coreApi.getBlockByHash(query).then(function(blockByHash) { - if (blockByHash) { - res.redirect("./block/" + query); - - return; - } - - req.session.userMessage = "No results found for query: " + query; - - res.redirect("./"); - + res.redirect("./block/" + query); }).catch(function(err) { req.session.userMessage = "No results found for query: " + query; @@ -584,12 +550,8 @@ router.post("/search", function(req, res, next) { } else if (!isNaN(query)) { coreApi.getBlockByHeight(parseInt(query)).then(function(blockByHeight) { - if (blockByHeight) { - res.redirect("./block-height/" + query); - - return; - } - + res.redirect("./block-height/" + query); + }).catch(function(err) { req.session.userMessage = "No results found for query: " + query; res.redirect("./"); @@ -815,6 +777,12 @@ router.get("/block-analysis", function(req, res, next) { next(); }); +router.get("/tx/:transactionId@:height", function(req, res, next) { + req.query.height = req.params.height; + req.url = "/tx/" + req.params.transactionId; + next(); +}) + router.get("/tx/:transactionId", function(req, res, next) { var txid = utils.asHash(req.params.transactionId); diff --git a/views/includes/block-content.pug b/views/includes/block-content.pug index fa9907ae9..8ad6f48ab 100644 --- a/views/includes/block-content.pug +++ b/views/includes/block-content.pug @@ -386,7 +386,7 @@ div.tab-content - var fontawesomeOutputName = "sign-out-alt"; div - - query_height = global.getindexinfo && !global.getindexinfo.txindex ? `?height=${result.getblock.height}` : '' + - tx_height_param = global.getindexinfo && !global.getindexinfo.txindex ? `@${result.getblock.height}` : '' each tx, txIndex in result.transactions //pre // code.json.bg-light #{JSON.stringify(tx, null, 4)} @@ -394,8 +394,8 @@ div.tab-content div.card-header.text-monospace if (tx && tx.txid) span(title=`Index in Block: #${(txIndex + offset).toLocaleString()}`, data-toggle="tooltip") ##{(txIndex + offset).toLocaleString()} - span – - a(href=("./tx/" + tx.txid + query_height)) #{tx.txid} + span – + a(href=("./tx/" + tx.txid + tx_height_param)) #{tx.txid} if (global.specialTransactions && global.specialTransactions[tx.txid]) span From 9f7a578635ba27a1b8efbe5413ba1b609cda8c2d Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Sun, 14 Feb 2021 06:32:42 +0200 Subject: [PATCH 07/11] Display fee as "unknown" instead of 0 --- views/transaction.pug | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/views/transaction.pug b/views/transaction.pug index 3dd12bcf7..69bb3b0c5 100644 --- a/views/transaction.pug +++ b/views/transaction.pug @@ -198,6 +198,12 @@ block content else small.data-tag.bg-primary #{minerInfo.name} + else if (global.getindexinfo && !global.getindexinfo.txindex) + div.row + div.summary-table-label Fee Paid + div.summary-table-content.text-monospace + | Unknown + span.ml-2(title="Determining the fee requires txindex to be enabled", data-toggle="tooltip"): i.fas.fa-ellipsis-h else - var feePaid = new Decimal(totalInputValue).minus(totalOutputValue); From f02f98e08bb7ca87107a191dc7dbabc3abb0541f Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Sun, 14 Feb 2021 07:24:46 +0200 Subject: [PATCH 08/11] Try looking up txids in wallet transactions and recent blocks --- .env-sample | 3 +++ app/api/rpcApi.js | 40 ++++++++++++++++++++++++++++++++++++++-- app/config.js | 2 ++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.env-sample b/.env-sample index 57210ba95..fa08d6460 100644 --- a/.env-sample +++ b/.env-sample @@ -110,6 +110,9 @@ # Default: 1024 #BTCEXP_OLD_SPACE_MAX_SIZE=2048 +# The number of recent blocks to search for transactions when txindex is disabled +#BTCEXP_NOTXINDEX_SEARCH_DEPTH=3 + # Show tools list in a sub-nav at top of screen # Default: true BTCEXP_UI_SHOW_TOOLS_SUBHEADER=true diff --git a/app/api/rpcApi.js b/app/api/rpcApi.js index e19e92698..7f16e4c42 100644 --- a/app/api/rpcApi.js +++ b/app/api/rpcApi.js @@ -224,7 +224,7 @@ function getAddress(address) { } function getRawTransaction(txid, blockhash) { - debugLog("getRawTransaction: %s", txid); + debugLog("getRawTransaction: %s %s", txid, blockhash); return new Promise(function(resolve, reject) { if (coins[config.coin].genesisCoinbaseTransactionIdsByNetwork[global.activeBlockchain] && txid == coins[config.coin].genesisCoinbaseTransactionIdsByNetwork[global.activeBlockchain]) { @@ -257,12 +257,48 @@ function getRawTransaction(txid, blockhash) { resolve(result); }).catch(function(err) { - reject(err); + if (global.getindexinfo && !global.getindexinfo.txindex && !blockhash) { + noTxIndexTransactionLookup(txid).then(resolve, reject); + } else { + reject(err); + } }); } }); } +async function noTxIndexTransactionLookup(txid) { + // Try looking up in wallet transactions + for (var wallet of await listWallets()) { + try { return await getWalletTransaction(wallet, txid); } + catch (_) {} + } + + // Try looking up in recent blocks + var tip_height = await getRpcDataWithParams({method:"getblockcount", parameters:[]}); + for (var height=tip_height; height>Math.max(tip_height - config.noTxIndexSearchDepth, 0); height--) { + var blockhash = await getRpcDataWithParams({method:"getblockhash", parameters:[height]}); + try { return await getRawTransaction(txid, blockhash); } + catch (_) {} + } + + throw new Error(`The requested tx ${txid} cannot be found in wallet transactions, mempool transactions and recently confirmed transactions`) +} + +function listWallets() { + return getRpcDataWithParams({method:"listwallets", parameters:[]}) +} + +async function getWalletTransaction(wallet, txid) { + global.rpcClient.wallet = wallet; + try { + return await getRpcDataWithParams({method:"gettransaction", parameters:[ txid, true, true ]}) + .then(wtx => ({ ...wtx, ...wtx.decoded, decoded: null })) + } finally { + global.rpcClient.wallet = null; + } +} + function getUtxo(txid, outputIndex) { debugLog("getUtxo: %s (%d)", txid, outputIndex); diff --git a/app/config.js b/app/config.js index cf11a3a26..daef33a03 100644 --- a/app/config.js +++ b/app/config.js @@ -92,6 +92,8 @@ module.exports = { rpcConcurrency: (process.env.BTCEXP_RPC_CONCURRENCY || 10), + noTxIndexSearchDepth: (+process.env.BTCEXP_NOTXINDEX_SEARCH_DEPTH || 3), + rpcBlacklist: process.env.BTCEXP_RPC_ALLOWALL.toLowerCase() == "true" ? [] : process.env.BTCEXP_RPC_BLACKLIST ? process.env.BTCEXP_RPC_BLACKLIST.split(',').filter(Boolean) From 0cef27af28c99ce9ed8b3b062942e18e431f265d Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Sun, 14 Feb 2021 05:51:52 +0200 Subject: [PATCH 09/11] Provide a more useful message when txindex is off Also changed session.userMessage to render as markdown, hopefully that's okay? --- routes/baseRouter.js | 8 +++++++- views/layout.pug | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/routes/baseRouter.js b/routes/baseRouter.js index 6b7c4b61d..a80f0c4f9 100644 --- a/routes/baseRouter.js +++ b/routes/baseRouter.js @@ -26,6 +26,8 @@ const v8 = require('v8'); const forceCsrf = csurf({ ignoreMethods: [] }); +var noTxIndexMsg = "\n\nYour node does not have `txindex` enabled. Without it, you can only lookup wallet, mempool and recently confirmed transactions by their `txid`. Searching for non-wallet transactions that were confirmed more than "+config.noTxIndexSearchDepth+" blocks ago is only possible if you provide the confirmed block height in addition to the txid, using `@` in the search box."; + router.get("/", function(req, res, next) { if (req.session.host == null || req.session.host.trim() == "") { if (req.cookies['rpc-host']) { @@ -543,7 +545,8 @@ router.post("/search", function(req, res, next) { res.redirect("./block/" + query); }).catch(function(err) { req.session.userMessage = "No results found for query: " + query; - + if (global.getindexinfo && !global.getindexinfo.txindex) + req.session.userMessage += noTxIndexMsg; res.redirect("./"); }); }); @@ -854,6 +857,9 @@ router.get("/tx/:transactionId", function(req, res, next) { }).catch(function(err) { res.locals.userMessageMarkdown = `Failed to load transaction: txid=**${txid}**`; + if (global.getindexinfo && !global.getindexinfo.txindex) + res.locals.userMessageMarkdown += noTxIndexMsg; + res.locals.pageErrors.push(utils.logError("1237y4ewssgt", err)); res.render("transaction"); diff --git a/views/layout.pug b/views/layout.pug index 1a56b4593..05a383268 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -154,7 +154,7 @@ html(lang="en") if (userMessage) div.alert(class=(userMessageType ? `alert-${userMessageType}` : "alert-warning"), role="alert") - span #{userMessage} + | !{markdown(userMessage)} div(style="min-height: 500px;") From 5526d185a617b9dbf641b02bea799ade8488c1d2 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Thu, 18 Feb 2021 11:05:38 +0200 Subject: [PATCH 10/11] Display fee of mempool transactions Using the information from `getmempooletry`. --- views/transaction.pug | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/views/transaction.pug b/views/transaction.pug index 69bb3b0c5..76c908c96 100644 --- a/views/transaction.pug +++ b/views/transaction.pug @@ -198,15 +198,14 @@ block content else small.data-tag.bg-primary #{minerInfo.name} - else if (global.getindexinfo && !global.getindexinfo.txindex) + else if (!mempoolDetails && global.getindexinfo && !global.getindexinfo.txindex) div.row div.summary-table-label Fee Paid div.summary-table-content.text-monospace | Unknown - span.ml-2(title="Determining the fee requires txindex to be enabled", data-toggle="tooltip"): i.fas.fa-ellipsis-h + span.ml-2(title="Determining the fee of confirmed transactions requires txindex to be enabled", data-toggle="tooltip"): i.fas.fa-ellipsis-h else - - - var feePaid = new Decimal(totalInputValue).minus(totalOutputValue); + - var feePaid = mempoolDetails ? new Decimal(mempoolDetails.entry.fees.base) : new Decimal(totalInputValue).minus(totalOutputValue); div.row div.summary-table-label Fee Paid @@ -214,7 +213,7 @@ block content - var currencyValue = feePaid; include includes/value-display.pug - if (feePaid > 0) + if (feePaid > 0 && totalInputValue > 0) - var item1 = utils.formatCurrencyAmount(totalInputValue, currencyFormatType); - var item2 = utils.formatCurrencyAmount(totalOutputValue, currencyFormatType); span.ml-2(title=(item1.simpleVal + " - " + item2.simpleVal), data-toggle="tooltip") @@ -225,16 +224,16 @@ block content div.summary-table-label Fee Rate div.summary-table-content.text-monospace if (result.getrawtransaction.vsize != result.getrawtransaction.size) - span #{utils.addThousandsSeparators(new DecimalRounded(totalInputValue).minus(totalOutputValue).dividedBy(result.getrawtransaction.vsize).times(100000000))} + span #{utils.addThousandsSeparators(new DecimalRounded(feePaid).dividedBy(result.getrawtransaction.vsize).times(100000000))} small sat/vB br - span.text-muted (#{utils.addThousandsSeparators(new DecimalRounded(totalInputValue).minus(totalOutputValue).dividedBy(result.getrawtransaction.size).times(100000000))} + span.text-muted (#{utils.addThousandsSeparators(new DecimalRounded(feePaid).dividedBy(result.getrawtransaction.size).times(100000000))} small sat/B span ) else - span #{utils.addThousandsSeparators(new DecimalRounded(totalInputValue).minus(totalOutputValue).dividedBy(result.getrawtransaction.size).times(100000000))} + span #{utils.addThousandsSeparators(new DecimalRounded(feePaid).dividedBy(result.getrawtransaction.size).times(100000000))} small sat/B - var daysDestroyed = new Decimal(0); From bea32a6af572087b7b2799856f25c5bf4086b5b3 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Fri, 19 Feb 2021 15:46:38 +0200 Subject: [PATCH 11/11] Lookup txid in external Electrum txindex This only works with Electrs and requires enabling BTCEXP_ELECTRUM_TXINDEX. See: https://github.com/romanz/electrs/commit/a0a3d4f9392e21f9e92fdc274c88fed6d0634794 --- .env-sample | 3 +++ app/api/electrumAddressApi.js | 22 +++++++++++++++++++++- app/api/rpcApi.js | 11 +++++++++++ app/config.js | 4 +++- 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.env-sample b/.env-sample index fa08d6460..ee5a3d2c2 100644 --- a/.env-sample +++ b/.env-sample @@ -44,6 +44,9 @@ # used if BTCEXP_ADDRESS_API=electrumx #BTCEXP_ELECTRUMX_SERVERS=tls://electrumx.server.com:50002,tcp://127.0.0.1:50001,... +# Use the Electrumx server as an external txindex. This is only available in Electrs. +#BTCEXP_ELECTRUM_TXINDEX=true + # Set number of concurrent RPC requests. Should be lower than your node's "rpcworkqueue" value. # Note that Bitcoin Core's default rpcworkqueue=16. # Default: 10 diff --git a/app/api/electrumAddressApi.js b/app/api/electrumAddressApi.js index d55e5c888..ed1dbbf40 100644 --- a/app/api/electrumAddressApi.js +++ b/app/api/electrumAddressApi.js @@ -323,6 +323,25 @@ function getAddressBalance(addrScripthash) { }); } +// Lookup the confirming block hash of a given txid. This only works with Electrs. +// https://github.com/romanz/electrs/commit/a0a3d4f9392e21f9e92fdc274c88fed6d0634794 +function lookupTxBlockHash(txid) { + if (electrumClients.length == 0) { + return Promise.reject({ error: "No ElectrumX Connection", userText: noConnectionsErrorText }); + } + + return runOnAllServers(function(electrumClient) { + return electrumClient.request('blockchain.transaction.get_confirmed_blockhash', [txid]); + }).then(function(results) { + var blockhash = results[0].result; + if (results.slice(1).every(({ result }) => result == blockhash)) { + return blockhash; + } else { + return Promise.reject({conflictedResults:results}); + } + }); +} + function logStats(cmd, dt, success) { if (!global.electrumStats.rpc[cmd]) { global.electrumStats.rpc[cmd] = {count:0, time:0, successes:0, failures:0}; @@ -341,6 +360,7 @@ function logStats(cmd, dt, success) { module.exports = { connectToServers: connectToServers, - getAddressDetails: getAddressDetails + getAddressDetails: getAddressDetails, + lookupTxBlockHash: lookupTxBlockHash, }; diff --git a/app/api/rpcApi.js b/app/api/rpcApi.js index 7f16e4c42..5335cd5ee 100644 --- a/app/api/rpcApi.js +++ b/app/api/rpcApi.js @@ -268,6 +268,17 @@ function getRawTransaction(txid, blockhash) { } async function noTxIndexTransactionLookup(txid) { + // Try looking up with an external Electrum server, using 'get_confirmed_blockhash'. + // This is only available in Electrs and requires enabling BTCEXP_ELECTRUM_TXINDEX. + if (config.addressApi == "electrumx" && config.electrumTxIndex) { + try { + var blockhash = await electrumAddressApi.lookupTxBlockHash(txid); + return await getRawTransaction(txid, blockhash); + } catch (err) { + debugLog(`Electrs blockhash lookup failed for ${txid}:`, err); + } + } + // Try looking up in wallet transactions for (var wallet of await listWallets()) { try { return await getWalletTransaction(wallet, txid); } diff --git a/app/config.js b/app/config.js index daef33a03..48fbecfe7 100644 --- a/app/config.js +++ b/app/config.js @@ -49,7 +49,8 @@ for (var i = 0; i < electrumXServerUriStrings.length; i++) { "BTCEXP_DEMO", "BTCEXP_PRIVACY_MODE", "BTCEXP_NO_INMEMORY_RPC_CACHE", - "BTCEXP_RPC_ALLOWALL" + "BTCEXP_RPC_ALLOWALL", + "BTCEXP_ELECTRUM_TXINDEX", ].forEach(function(item) { if (process.env[item] === undefined) { @@ -170,6 +171,7 @@ module.exports = { ], addressApi:process.env.BTCEXP_ADDRESS_API, + electrumTxIndex:process.env.BTCEXP_ELECTRUM_TXINDEX != "false", electrumXServers:electrumXServers, redisUrl:process.env.BTCEXP_REDIS_URL,