|
13 | 13 | #include <consensus/validation.h>
|
14 | 14 | #include <validation.h>
|
15 | 15 | #include <core_io.h>
|
| 16 | +#include <index/txindex.h> |
16 | 17 | #include <policy/feerate.h>
|
17 | 18 | #include <policy/policy.h>
|
18 | 19 | #include <primitives/transaction.h>
|
|
31 | 32 |
|
32 | 33 | #include <univalue.h>
|
33 | 34 |
|
| 35 | +#include <boost/algorithm/string.hpp> |
34 | 36 | #include <boost/thread/thread.hpp> // boost::thread::interrupt
|
35 | 37 |
|
36 | 38 | #include <memory>
|
@@ -737,6 +739,25 @@ static UniValue getblockheader(const JSONRPCRequest& request)
|
737 | 739 | return blockheaderToJSON(pblockindex);
|
738 | 740 | }
|
739 | 741 |
|
| 742 | +static CBlock GetBlockChecked(const CBlockIndex* pblockindex) |
| 743 | +{ |
| 744 | + CBlock block; |
| 745 | + if (fHavePruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0) { |
| 746 | + throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)"); |
| 747 | + } |
| 748 | + |
| 749 | + if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) { |
| 750 | + // Block not found on disk. This could be because we have the block |
| 751 | + // header in our index but don't have the block (for example if a |
| 752 | + // non-whitelisted node sends us an unrequested long chain of valid |
| 753 | + // blocks, we add the headers to our index, but don't accept the |
| 754 | + // block). |
| 755 | + throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk"); |
| 756 | + } |
| 757 | + |
| 758 | + return block; |
| 759 | +} |
| 760 | + |
740 | 761 | static UniValue getblock(const JSONRPCRequest& request)
|
741 | 762 | {
|
742 | 763 | if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
|
@@ -805,17 +826,7 @@ static UniValue getblock(const JSONRPCRequest& request)
|
805 | 826 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
|
806 | 827 | }
|
807 | 828 |
|
808 |
| - CBlock block; |
809 |
| - if (fHavePruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0) |
810 |
| - throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)"); |
811 |
| - |
812 |
| - if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) |
813 |
| - // Block not found on disk. This could be because we have the block |
814 |
| - // header in our index but don't have the block (for example if a |
815 |
| - // non-whitelisted node sends us an unrequested long chain of valid |
816 |
| - // blocks, we add the headers to our index, but don't accept the |
817 |
| - // block). |
818 |
| - throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk"); |
| 829 | + const CBlock block = GetBlockChecked(pblockindex); |
819 | 830 |
|
820 | 831 | if (verbosity <= 0)
|
821 | 832 | {
|
@@ -1614,6 +1625,284 @@ static UniValue getchaintxstats(const JSONRPCRequest& request)
|
1614 | 1625 | return ret;
|
1615 | 1626 | }
|
1616 | 1627 |
|
| 1628 | +template<typename T> |
| 1629 | +static T CalculateTruncatedMedian(std::vector<T>& scores) |
| 1630 | +{ |
| 1631 | + size_t size = scores.size(); |
| 1632 | + if (size == 0) { |
| 1633 | + return 0; |
| 1634 | + } |
| 1635 | + |
| 1636 | + std::sort(scores.begin(), scores.end()); |
| 1637 | + if (size % 2 == 0) { |
| 1638 | + return (scores[size / 2 - 1] + scores[size / 2]) / 2; |
| 1639 | + } else { |
| 1640 | + return scores[size / 2]; |
| 1641 | + } |
| 1642 | +} |
| 1643 | + |
| 1644 | +template<typename T> |
| 1645 | +static inline bool SetHasKeys(const std::set<T>& set) {return false;} |
| 1646 | +template<typename T, typename Tk, typename... Args> |
| 1647 | +static inline bool SetHasKeys(const std::set<T>& set, const Tk& key, const Args&... args) |
| 1648 | +{ |
| 1649 | + return (set.count(key) != 0) || SetHasKeys(set, args...); |
| 1650 | +} |
| 1651 | + |
| 1652 | +// outpoint (needed for the utxo index) + nHeight + fCoinBase |
| 1653 | +static constexpr size_t PER_UTXO_OVERHEAD = sizeof(COutPoint) + sizeof(uint32_t) + sizeof(bool); |
| 1654 | + |
| 1655 | +static UniValue getblockstats(const JSONRPCRequest& request) |
| 1656 | +{ |
| 1657 | + if (request.fHelp || request.params.size() < 1 || request.params.size() > 4) { |
| 1658 | + throw std::runtime_error( |
| 1659 | + "getblockstats hash_or_height ( stats )\n" |
| 1660 | + "\nCompute per block statistics for a given window. All amounts are in satoshis.\n" |
| 1661 | + "It won't work for some heights with pruning.\n" |
| 1662 | + "It won't work without -txindex for utxo_size_inc, *fee or *feerate stats.\n" |
| 1663 | + "\nArguments:\n" |
| 1664 | + "1. \"hash_or_height\" (string or numeric, required) The block hash or height of the target block\n" |
| 1665 | + "2. \"stats\" (array, optional) Values to plot, by default all values (see result below)\n" |
| 1666 | + " [\n" |
| 1667 | + " \"height\", (string, optional) Selected statistic\n" |
| 1668 | + " \"time\", (string, optional) Selected statistic\n" |
| 1669 | + " ,...\n" |
| 1670 | + " ]\n" |
| 1671 | + "\nResult:\n" |
| 1672 | + "{ (json object)\n" |
| 1673 | + " \"avgfee\": xxxxx, (numeric) Average fee in the block\n" |
| 1674 | + " \"avgfeerate\": xxxxx, (numeric) Average feerate (in satoshis per virtual byte)\n" |
| 1675 | + " \"avgtxsize\": xxxxx, (numeric) Average transaction size\n" |
| 1676 | + " \"blockhash\": xxxxx, (string) The block hash (to check for potential reorgs)\n" |
| 1677 | + " \"height\": xxxxx, (numeric) The height of the block\n" |
| 1678 | + " \"ins\": xxxxx, (numeric) The number of inputs (excluding coinbase)\n" |
| 1679 | + " \"maxfee\": xxxxx, (numeric) Maximum fee in the block\n" |
| 1680 | + " \"maxfeerate\": xxxxx, (numeric) Maximum feerate (in satoshis per virtual byte)\n" |
| 1681 | + " \"maxtxsize\": xxxxx, (numeric) Maximum transaction size\n" |
| 1682 | + " \"medianfee\": xxxxx, (numeric) Truncated median fee in the block\n" |
| 1683 | + " \"medianfeerate\": xxxxx, (numeric) Truncated median feerate (in satoshis per virtual byte)\n" |
| 1684 | + " \"mediantime\": xxxxx, (numeric) The block median time past\n" |
| 1685 | + " \"mediantxsize\": xxxxx, (numeric) Truncated median transaction size\n" |
| 1686 | + " \"minfee\": xxxxx, (numeric) Minimum fee in the block\n" |
| 1687 | + " \"minfeerate\": xxxxx, (numeric) Minimum feerate (in satoshis per virtual byte)\n" |
| 1688 | + " \"mintxsize\": xxxxx, (numeric) Minimum transaction size\n" |
| 1689 | + " \"outs\": xxxxx, (numeric) The number of outputs\n" |
| 1690 | + " \"subsidy\": xxxxx, (numeric) The block subsidy\n" |
| 1691 | + " \"swtotal_size\": xxxxx, (numeric) Total size of all segwit transactions\n" |
| 1692 | + " \"swtotal_weight\": xxxxx, (numeric) Total weight of all segwit transactions divided by segwit scale factor (4)\n" |
| 1693 | + " \"swtxs\": xxxxx, (numeric) The number of segwit transactions\n" |
| 1694 | + " \"time\": xxxxx, (numeric) The block time\n" |
| 1695 | + " \"total_out\": xxxxx, (numeric) Total amount in all outputs (excluding coinbase and thus reward [ie subsidy + totalfee])\n" |
| 1696 | + " \"total_size\": xxxxx, (numeric) Total size of all non-coinbase transactions\n" |
| 1697 | + " \"total_weight\": xxxxx, (numeric) Total weight of all non-coinbase transactions divided by segwit scale factor (4)\n" |
| 1698 | + " \"totalfee\": xxxxx, (numeric) The fee total\n" |
| 1699 | + " \"txs\": xxxxx, (numeric) The number of transactions (excluding coinbase)\n" |
| 1700 | + " \"utxo_increase\": xxxxx, (numeric) The increase/decrease in the number of unspent outputs\n" |
| 1701 | + " \"utxo_size_inc\": xxxxx, (numeric) The increase/decrease in size for the utxo index (not discounting op_return and similar)\n" |
| 1702 | + "}\n" |
| 1703 | + "\nExamples:\n" |
| 1704 | + + HelpExampleCli("getblockstats", "1000 '[\"minfeerate\",\"avgfeerate\"]'") |
| 1705 | + + HelpExampleRpc("getblockstats", "1000 '[\"minfeerate\",\"avgfeerate\"]'") |
| 1706 | + ); |
| 1707 | + } |
| 1708 | + |
| 1709 | + LOCK(cs_main); |
| 1710 | + |
| 1711 | + CBlockIndex* pindex; |
| 1712 | + if (request.params[0].isNum()) { |
| 1713 | + const int height = request.params[0].get_int(); |
| 1714 | + const int current_tip = chainActive.Height(); |
| 1715 | + if (height < 0) { |
| 1716 | + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Target block height %d is negative", height)); |
| 1717 | + } |
| 1718 | + if (height > current_tip) { |
| 1719 | + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Target block height %d after current tip %d", height, current_tip)); |
| 1720 | + } |
| 1721 | + |
| 1722 | + pindex = chainActive[height]; |
| 1723 | + } else { |
| 1724 | + const std::string strHash = request.params[0].get_str(); |
| 1725 | + const uint256 hash(uint256S(strHash)); |
| 1726 | + pindex = LookupBlockIndex(hash); |
| 1727 | + if (!pindex) { |
| 1728 | + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); |
| 1729 | + } |
| 1730 | + if (!chainActive.Contains(pindex)) { |
| 1731 | + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Block is not in chain %s", Params().NetworkIDString())); |
| 1732 | + } |
| 1733 | + } |
| 1734 | + |
| 1735 | + assert(pindex != nullptr); |
| 1736 | + |
| 1737 | + std::set<std::string> stats; |
| 1738 | + if (!request.params[1].isNull()) { |
| 1739 | + const UniValue stats_univalue = request.params[1].get_array(); |
| 1740 | + for (unsigned int i = 0; i < stats_univalue.size(); i++) { |
| 1741 | + const std::string stat = stats_univalue[i].get_str(); |
| 1742 | + stats.insert(stat); |
| 1743 | + } |
| 1744 | + } |
| 1745 | + |
| 1746 | + const CBlock block = GetBlockChecked(pindex); |
| 1747 | + |
| 1748 | + const bool do_all = stats.size() == 0; // Calculate everything if nothing selected (default) |
| 1749 | + const bool do_mediantxsize = do_all || stats.count("mediantxsize") != 0; |
| 1750 | + const bool do_medianfee = do_all || stats.count("medianfee") != 0; |
| 1751 | + const bool do_medianfeerate = do_all || stats.count("medianfeerate") != 0; |
| 1752 | + const bool loop_inputs = do_all || do_medianfee || do_medianfeerate || |
| 1753 | + SetHasKeys(stats, "utxo_size_inc", "totalfee", "avgfee", "avgfeerate", "minfee", "maxfee", "minfeerate", "maxfeerate"); |
| 1754 | + const bool loop_outputs = do_all || loop_inputs || stats.count("total_out"); |
| 1755 | + const bool do_calculate_size = do_mediantxsize || |
| 1756 | + SetHasKeys(stats, "total_size", "avgtxsize", "mintxsize", "maxtxsize", "swtotal_size"); |
| 1757 | + const bool do_calculate_weight = do_all || SetHasKeys(stats, "total_weight", "avgfeerate", "swtotal_weight", "avgfeerate", "medianfeerate", "minfeerate", "maxfeerate"); |
| 1758 | + const bool do_calculate_sw = do_all || SetHasKeys(stats, "swtxs", "swtotal_size", "swtotal_weight"); |
| 1759 | + |
| 1760 | + CAmount maxfee = 0; |
| 1761 | + CAmount maxfeerate = 0; |
| 1762 | + CAmount minfee = MAX_MONEY; |
| 1763 | + CAmount minfeerate = MAX_MONEY; |
| 1764 | + CAmount total_out = 0; |
| 1765 | + CAmount totalfee = 0; |
| 1766 | + int64_t inputs = 0; |
| 1767 | + int64_t maxtxsize = 0; |
| 1768 | + int64_t mintxsize = MAX_BLOCK_SERIALIZED_SIZE; |
| 1769 | + int64_t outputs = 0; |
| 1770 | + int64_t swtotal_size = 0; |
| 1771 | + int64_t swtotal_weight = 0; |
| 1772 | + int64_t swtxs = 0; |
| 1773 | + int64_t total_size = 0; |
| 1774 | + int64_t total_weight = 0; |
| 1775 | + int64_t utxo_size_inc = 0; |
| 1776 | + std::vector<CAmount> fee_array; |
| 1777 | + std::vector<CAmount> feerate_array; |
| 1778 | + std::vector<int64_t> txsize_array; |
| 1779 | + |
| 1780 | + for (const auto& tx : block.vtx) { |
| 1781 | + outputs += tx->vout.size(); |
| 1782 | + |
| 1783 | + CAmount tx_total_out = 0; |
| 1784 | + if (loop_outputs) { |
| 1785 | + for (const CTxOut& out : tx->vout) { |
| 1786 | + tx_total_out += out.nValue; |
| 1787 | + utxo_size_inc += GetSerializeSize(out, SER_NETWORK, PROTOCOL_VERSION) + PER_UTXO_OVERHEAD; |
| 1788 | + } |
| 1789 | + } |
| 1790 | + |
| 1791 | + if (tx->IsCoinBase()) { |
| 1792 | + continue; |
| 1793 | + } |
| 1794 | + |
| 1795 | + inputs += tx->vin.size(); // Don't count coinbase's fake input |
| 1796 | + total_out += tx_total_out; // Don't count coinbase reward |
| 1797 | + |
| 1798 | + int64_t tx_size = 0; |
| 1799 | + if (do_calculate_size) { |
| 1800 | + |
| 1801 | + tx_size = tx->GetTotalSize(); |
| 1802 | + if (do_mediantxsize) { |
| 1803 | + txsize_array.push_back(tx_size); |
| 1804 | + } |
| 1805 | + maxtxsize = std::max(maxtxsize, tx_size); |
| 1806 | + mintxsize = std::min(mintxsize, tx_size); |
| 1807 | + total_size += tx_size; |
| 1808 | + } |
| 1809 | + |
| 1810 | + int64_t weight = 0; |
| 1811 | + if (do_calculate_weight) { |
| 1812 | + weight = GetTransactionWeight(*tx); |
| 1813 | + total_weight += weight; |
| 1814 | + } |
| 1815 | + |
| 1816 | + if (do_calculate_sw && tx->HasWitness()) { |
| 1817 | + ++swtxs; |
| 1818 | + swtotal_size += tx_size; |
| 1819 | + swtotal_weight += weight; |
| 1820 | + } |
| 1821 | + |
| 1822 | + if (loop_inputs) { |
| 1823 | + |
| 1824 | + if (!g_txindex) { |
| 1825 | + throw JSONRPCError(RPC_INVALID_PARAMETER, "One or more of the selected stats requires -txindex enabled"); |
| 1826 | + } |
| 1827 | + CAmount tx_total_in = 0; |
| 1828 | + for (const CTxIn& in : tx->vin) { |
| 1829 | + CTransactionRef tx_in; |
| 1830 | + uint256 hashBlock; |
| 1831 | + if (!GetTransaction(in.prevout.hash, tx_in, Params().GetConsensus(), hashBlock, false)) { |
| 1832 | + throw JSONRPCError(RPC_INTERNAL_ERROR, std::string("Unexpected internal error (tx index seems corrupt)")); |
| 1833 | + } |
| 1834 | + |
| 1835 | + CTxOut prevoutput = tx_in->vout[in.prevout.n]; |
| 1836 | + |
| 1837 | + tx_total_in += prevoutput.nValue; |
| 1838 | + utxo_size_inc -= GetSerializeSize(prevoutput, SER_NETWORK, PROTOCOL_VERSION) + PER_UTXO_OVERHEAD; |
| 1839 | + } |
| 1840 | + |
| 1841 | + CAmount txfee = tx_total_in - tx_total_out; |
| 1842 | + assert(MoneyRange(txfee)); |
| 1843 | + if (do_medianfee) { |
| 1844 | + fee_array.push_back(txfee); |
| 1845 | + } |
| 1846 | + maxfee = std::max(maxfee, txfee); |
| 1847 | + minfee = std::min(minfee, txfee); |
| 1848 | + totalfee += txfee; |
| 1849 | + |
| 1850 | + // New feerate uses satoshis per virtual byte instead of per serialized byte |
| 1851 | + CAmount feerate = weight ? (txfee * WITNESS_SCALE_FACTOR) / weight : 0; |
| 1852 | + if (do_medianfeerate) { |
| 1853 | + feerate_array.push_back(feerate); |
| 1854 | + } |
| 1855 | + maxfeerate = std::max(maxfeerate, feerate); |
| 1856 | + minfeerate = std::min(minfeerate, feerate); |
| 1857 | + } |
| 1858 | + } |
| 1859 | + |
| 1860 | + UniValue ret_all(UniValue::VOBJ); |
| 1861 | + ret_all.pushKV("avgfee", (block.vtx.size() > 1) ? totalfee / (block.vtx.size() - 1) : 0); |
| 1862 | + ret_all.pushKV("avgfeerate", total_weight ? (totalfee * WITNESS_SCALE_FACTOR) / total_weight : 0); // Unit: sat/vbyte |
| 1863 | + ret_all.pushKV("avgtxsize", (block.vtx.size() > 1) ? total_size / (block.vtx.size() - 1) : 0); |
| 1864 | + ret_all.pushKV("blockhash", pindex->GetBlockHash().GetHex()); |
| 1865 | + ret_all.pushKV("height", (int64_t)pindex->nHeight); |
| 1866 | + ret_all.pushKV("ins", inputs); |
| 1867 | + ret_all.pushKV("maxfee", maxfee); |
| 1868 | + ret_all.pushKV("maxfeerate", maxfeerate); |
| 1869 | + ret_all.pushKV("maxtxsize", maxtxsize); |
| 1870 | + ret_all.pushKV("medianfee", CalculateTruncatedMedian(fee_array)); |
| 1871 | + ret_all.pushKV("medianfeerate", CalculateTruncatedMedian(feerate_array)); |
| 1872 | + ret_all.pushKV("mediantime", pindex->GetMedianTimePast()); |
| 1873 | + ret_all.pushKV("mediantxsize", CalculateTruncatedMedian(txsize_array)); |
| 1874 | + ret_all.pushKV("minfee", (minfee == MAX_MONEY) ? 0 : minfee); |
| 1875 | + ret_all.pushKV("minfeerate", (minfeerate == MAX_MONEY) ? 0 : minfeerate); |
| 1876 | + ret_all.pushKV("mintxsize", mintxsize == MAX_BLOCK_SERIALIZED_SIZE ? 0 : mintxsize); |
| 1877 | + ret_all.pushKV("outs", outputs); |
| 1878 | + ret_all.pushKV("subsidy", GetBlockSubsidy(pindex->nHeight, Params().GetConsensus())); |
| 1879 | + ret_all.pushKV("swtotal_size", swtotal_size); |
| 1880 | + ret_all.pushKV("swtotal_weight", swtotal_weight); |
| 1881 | + ret_all.pushKV("swtxs", swtxs); |
| 1882 | + ret_all.pushKV("time", pindex->GetBlockTime()); |
| 1883 | + ret_all.pushKV("total_out", total_out); |
| 1884 | + ret_all.pushKV("total_size", total_size); |
| 1885 | + ret_all.pushKV("total_weight", total_weight); |
| 1886 | + ret_all.pushKV("totalfee", totalfee); |
| 1887 | + ret_all.pushKV("txs", (int64_t)block.vtx.size()); |
| 1888 | + ret_all.pushKV("utxo_increase", outputs - inputs); |
| 1889 | + ret_all.pushKV("utxo_size_inc", utxo_size_inc); |
| 1890 | + |
| 1891 | + if (do_all) { |
| 1892 | + return ret_all; |
| 1893 | + } |
| 1894 | + |
| 1895 | + UniValue ret(UniValue::VOBJ); |
| 1896 | + for (const std::string& stat : stats) { |
| 1897 | + const UniValue& value = ret_all[stat]; |
| 1898 | + if (value.isNull()) { |
| 1899 | + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid selected statistic %s", stat)); |
| 1900 | + } |
| 1901 | + ret.pushKV(stat, value); |
| 1902 | + } |
| 1903 | + return ret; |
| 1904 | +} |
| 1905 | + |
1617 | 1906 | static UniValue savemempool(const JSONRPCRequest& request)
|
1618 | 1907 | {
|
1619 | 1908 | if (request.fHelp || request.params.size() != 0) {
|
@@ -1642,6 +1931,7 @@ static const CRPCCommand commands[] =
|
1642 | 1931 | // --------------------- ------------------------ ----------------------- ----------
|
1643 | 1932 | { "blockchain", "getblockchaininfo", &getblockchaininfo, {} },
|
1644 | 1933 | { "blockchain", "getchaintxstats", &getchaintxstats, {"nblocks", "blockhash"} },
|
| 1934 | + { "blockchain", "getblockstats", &getblockstats, {"hash_or_height", "stats"} }, |
1645 | 1935 | { "blockchain", "getbestblockhash", &getbestblockhash, {} },
|
1646 | 1936 | { "blockchain", "getblockcount", &getblockcount, {} },
|
1647 | 1937 | { "blockchain", "getblock", &getblock, {"blockhash","verbosity|verbose"} },
|
|
0 commit comments