diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -11,3 +11,7 @@ the RPC commands `walletcreatefundedpsbt` and `fundrawtransaction` will now fail instead of rounding down the fee. Beware that the `feeRate` argument is specified in BCH per kilobyte, not satoshi per byte. + +RPC changes +----------- +The `getblockstats` RPC is faster for fee calculation by using BlockUndo data. Also, `-txindex` is no longer required and `getblockstats` works for all non-pruned blocks. diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -919,6 +920,20 @@ return block; } +static CBlockUndo GetUndoChecked(const CBlockIndex *pblockindex) { + CBlockUndo blockUndo; + if (IsBlockPruned(pblockindex)) { + throw JSONRPCError(RPC_MISC_ERROR, + "Undo data not available (pruned data)"); + } + + if (!UndoReadFromDisk(blockUndo, pblockindex)) { + throw JSONRPCError(RPC_MISC_ERROR, "Can't read undo data from disk"); + } + + return blockUndo; +} + static UniValue getblock(const Config &config, const JSONRPCRequest &request) { if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) { @@ -2059,9 +2074,7 @@ "in " + CURRENCY_UNIT + ".\n" - "It won't work for some heights with pruning.\n" - "It won't work without -txindex for utxo_size_inc, *fee or " - "*feerate stats.\n", + "It won't work for some heights with pruning.\n", { {"hash_or_height", RPCArg::Type::NUM, @@ -2183,6 +2196,7 @@ } const CBlock block = GetBlockChecked(config, pindex); + const CBlockUndo blockUndo = GetUndoChecked(pindex); // Calculate everything if nothing selected (default) const bool do_all = stats.size() == 0; @@ -2198,12 +2212,6 @@ do_mediantxsize || loop_inputs || SetHasKeys(stats, "total_size", "avgtxsize", "mintxsize", "maxtxsize"); - if (loop_inputs && !g_txindex) { - throw JSONRPCError( - RPC_INVALID_PARAMETER, - "One or more of the selected stats requires -txindex enabled"); - } - const int64_t blockMaxSize = config.GetMaxBlockSize(); Amount maxfee = Amount::zero(); Amount maxfeerate = Amount::zero(); @@ -2221,9 +2229,8 @@ std::vector feerate_array; std::vector txsize_array; - const Consensus::Params ¶ms = config.GetChainParams().GetConsensus(); - - for (const auto &tx : block.vtx) { + for (size_t i = 0; i < block.vtx.size(); ++i) { + const auto &tx = block.vtx.at(i); outputs += tx->vout.size(); Amount tx_total_out = Amount::zero(); if (loop_outputs) { @@ -2256,17 +2263,9 @@ if (loop_inputs) { Amount tx_total_in = Amount::zero(); - for (const CTxIn &in : tx->vin) { - CTransactionRef tx_in; - BlockHash hashBlock; - if (!GetTransaction(in.prevout.GetTxId(), tx_in, params, - hashBlock)) { - throw JSONRPCError(RPC_INTERNAL_ERROR, - std::string("Unexpected internal error " - "(tx index seems corrupt)")); - } - - CTxOut prevoutput = tx_in->vout[in.prevout.GetN()]; + const auto &txundo = blockUndo.vtxundo.at(i - 1); + for (const Coin &coin : txundo.vprevout) { + const CTxOut &prevoutput = coin.GetTxOut(); tx_total_in += prevoutput.nValue; utxo_size_inc -= diff --git a/test/functional/rpc_getblockstats.py b/test/functional/rpc_getblockstats.py --- a/test/functional/rpc_getblockstats.py +++ b/test/functional/rpc_getblockstats.py @@ -9,7 +9,6 @@ import decimal import json import os -import time from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -32,18 +31,6 @@ start_height = 101 max_stat_pos = 2 - STATS_NEED_TXINDEX = [ - 'avgfee', - 'avgfeerate', - 'maxfee', - 'maxfeerate', - 'medianfee', - 'medianfeerate', - 'minfee', - 'minfeerate', - 'totalfee', - 'utxo_size_inc', - ] def add_options(self, parser): parser.add_argument('--gen-test-data', dest='gen_test_data', @@ -55,8 +42,7 @@ help='Test data file') def set_test_params(self): - self.num_nodes = 2 - self.extra_args = [['-txindex'], ['-paytxfee=0.003']] + self.num_nodes = 1 self.setup_clean_chain = True def get_stats(self): @@ -64,20 +50,31 @@ hash_or_height=self.start_height + i) for i in range(self.max_stat_pos + 1)] def generate_test_data(self, filename): - mocktime = time.time() + mocktime = 1525107225 + self.nodes[0].setmocktime(mocktime) self.nodes[0].generate(101) + address = self.nodes[0].get_deterministic_priv_key().address self.nodes[0].sendtoaddress( - address=self.nodes[1].getnewaddress(), amount=10, subtractfeefromamount=True) + address=address, + amount=10, + subtractfeefromamount=True) self.nodes[0].generate(1) self.sync_all() self.nodes[0].sendtoaddress( - address=self.nodes[0].getnewaddress(), amount=10, subtractfeefromamount=True) + address=address, + amount=10, + subtractfeefromamount=True) + self.nodes[0].sendtoaddress( + address=address, + amount=10, + subtractfeefromamount=False) + self.nodes[0].settxfee(amount=0.003) self.nodes[0].sendtoaddress( - address=self.nodes[0].getnewaddress(), amount=10, subtractfeefromamount=False) - self.nodes[1].sendtoaddress( - address=self.nodes[0].getnewaddress(), amount=1, subtractfeefromamount=True) + address=address, + amount=1, + subtractfeefromamount=True) self.sync_all() self.nodes[0].generate(1) @@ -112,7 +109,7 @@ # Set the timestamps from the file so that the nodes can get out of # Initial Block Download self.nodes[0].setmocktime(mocktime) - self.nodes[1].setmocktime(mocktime) + self.sync_all() for i, b in enumerate(blocks): self.nodes[0].submitblock(b) @@ -126,10 +123,6 @@ self.sync_all() stats = self.get_stats() - expected_stats_noindex = [] - for stat_row in stats: - expected_stats_noindex.append( - {k: v for k, v in stat_row.items() if k not in self.STATS_NEED_TXINDEX}) # Make sure all valid statistics are included but nothing else is expected_keys = self.expected_stats[0].keys() @@ -149,11 +142,6 @@ hash_or_height=blockhash) assert_equal(stats_by_hash, self.expected_stats[i]) - # Check with the node that has no txindex - stats_no_txindex = self.nodes[1].getblockstats( - hash_or_height=blockhash, stats=list(expected_stats_noindex[i].keys())) - assert_equal(stats_no_txindex, expected_stats_noindex[i]) - # Make sure each stat can be queried on its own for stat in expected_keys: for i in range(self.max_stat_pos + 1): @@ -191,14 +179,6 @@ inv_sel_stat), self.nodes[0].getblockstats, hash_or_height=1, stats=inv_stat) # Make sure we aren't always returning inv_sel_stat as the culprit stat - assert_raises_rpc_error(-8, 'Invalid selected statistic aaa{}'.format(inv_sel_stat), - self.nodes[0].getblockstats, hash_or_height=1, stats=['minfee', 'aaa{}'.format(inv_sel_stat)]) - - assert_raises_rpc_error(-8, 'One or more of the selected stats requires -txindex enabled', - self.nodes[1].getblockstats, hash_or_height=1) - assert_raises_rpc_error(-8, 'One or more of the selected stats requires -txindex enabled', - self.nodes[1].getblockstats, hash_or_height=self.start_height + self.max_stat_pos) - # Mainchain's genesis block shouldn't be found on regtest assert_raises_rpc_error(-5, 'Block not found', self.nodes[0].getblockstats, hash_or_height='000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f')