diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -5,3 +5,5 @@ This release includes the following features and fixes: + - The `gettxoutsetinfo` RPC now accepts `'muhash'` as a value for the `hash_type` + input parameter, in addition to `'none'` and `'hash_serialized'`. diff --git a/src/node/coinstats.h b/src/node/coinstats.h --- a/src/node/coinstats.h +++ b/src/node/coinstats.h @@ -17,6 +17,7 @@ enum class CoinStatsHashType { HASH_SERIALIZED, + MUHASH, NONE, }; diff --git a/src/node/coinstats.cpp b/src/node/coinstats.cpp --- a/src/node/coinstats.cpp +++ b/src/node/coinstats.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -42,6 +43,19 @@ const std::map &outputs, std::map::const_iterator it) {} +static void ApplyHash(CCoinsStats &stats, MuHash3072 &muhash, const TxId &txid, + const std::map &outputs, + std::map::const_iterator it) { + COutPoint outpoint = COutPoint(txid, it->first); + Coin coin = it->second; + + CDataStream ss(SER_DISK, PROTOCOL_VERSION); + ss << outpoint; + ss << static_cast(coin.GetHeight() * 2 + coin.IsCoinBase()); + ss << coin.GetTxOut(); + muhash.Insert(MakeUCharSpan(ss)); +} + template static void ApplyStats(CCoinsStats &stats, T &hash_obj, const TxId &txid, const std::map &outputs) { @@ -109,6 +123,10 @@ CHashWriter ss(SER_GETHASH, PROTOCOL_VERSION); return GetUTXOStats(view, stats, ss, interruption_point); } + case (CoinStatsHashType::MUHASH): { + MuHash3072 muhash; + return GetUTXOStats(view, stats, muhash, interruption_point); + } case (CoinStatsHashType::NONE): { return GetUTXOStats(view, stats, nullptr, interruption_point); } @@ -120,9 +138,16 @@ static void PrepareHash(CHashWriter &ss, const CCoinsStats &stats) { ss << stats.hashBlock; } +// MuHash does not need the prepare step +static void PrepareHash(MuHash3072 &muhash, CCoinsStats &stats) {} static void PrepareHash(std::nullptr_t, CCoinsStats &stats) {} static void FinalizeHash(CHashWriter &ss, CCoinsStats &stats) { stats.hashSerialized = ss.GetHash(); } +static void FinalizeHash(MuHash3072 &muhash, CCoinsStats &stats) { + uint256 out; + muhash.Finalize(out); + stats.hashSerialized = out; +} static void FinalizeHash(std::nullptr_t, CCoinsStats &stats) {} diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1232,6 +1232,20 @@ }; } +static CoinStatsHashType ParseHashType(const std::string &hash_type_input) { + if (hash_type_input == "hash_serialized") { + return CoinStatsHashType::HASH_SERIALIZED; + } else if (hash_type_input == "muhash") { + return CoinStatsHashType::MUHASH; + } else if (hash_type_input == "none") { + return CoinStatsHashType::NONE; + } else { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + strprintf("%s is not a valid hash_type", hash_type_input)); + } +} + static RPCHelpMan gettxoutsetinfo() { return RPCHelpMan{ "gettxoutsetinfo", @@ -1240,7 +1254,7 @@ { {"hash_type", RPCArg::Type::STR, /* default */ "hash_serialized", "Which UTXO set hash should be calculated. Options: " - "'hash_serialized' (the legacy algorithm), 'none'."}, + "'hash_serialized' (the legacy algorithm), 'muhash', 'none'."}, }, RPCResult{RPCResult::Type::OBJ, "", @@ -1257,8 +1271,12 @@ {RPCResult::Type::NUM, "bogosize", "A meaningless metric for UTXO set size"}, {RPCResult::Type::STR_HEX, "hash_serialized", + /* optional */ true, "The serialized hash (only present if 'hash_serialized' " "hash_type is chosen)"}, + {RPCResult::Type::STR_HEX, "muhash", /* optional */ true, + "The serialized hash (only present if 'muhash' " + "hash_type is chosen)"}, {RPCResult::Type::NUM, "disk_size", "The estimated size of the chainstate on disk"}, {RPCResult::Type::STR_AMOUNT, "total_amount", @@ -1273,8 +1291,10 @@ CCoinsStats stats; ::ChainstateActive().ForceFlushStateToDisk(); - const CoinStatsHashType hash_type = ParseHashType( - request.params[0], CoinStatsHashType::HASH_SERIALIZED); + const CoinStatsHashType hash_type{ + request.params[0].isNull() + ? CoinStatsHashType::HASH_SERIALIZED + : ParseHashType(request.params[0].get_str())}; CCoinsView *coins_view = WITH_LOCK(cs_main, return &ChainstateActive().CoinsDB()); @@ -1290,6 +1310,9 @@ ret.pushKV("hash_serialized", stats.hashSerialized.GetHex()); } + if (hash_type == CoinStatsHashType::MUHASH) { + ret.pushKV("muhash", stats.hashSerialized.GetHex()); + } ret.pushKV("disk_size", stats.nDiskSize); ret.pushKV("total_amount", stats.nTotalAmount); } else { diff --git a/src/rpc/util.h b/src/rpc/util.h --- a/src/rpc/util.h +++ b/src/rpc/util.h @@ -80,9 +80,6 @@ extern std::vector ParseHexV(const UniValue &v, std::string strName); extern std::vector ParseHexO(const UniValue &o, std::string strKey); -CoinStatsHashType ParseHashType(const UniValue ¶m, - const CoinStatsHashType default_type); - extern Amount AmountFromValue(const UniValue &value); extern std::string HelpExampleCli(const std::string &methodname, const std::string &args); diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -135,25 +135,6 @@ return ParseHexV(find_value(o, strKey), strKey); } -CoinStatsHashType ParseHashType(const UniValue ¶m, - const CoinStatsHashType default_type) { - if (param.isNull()) { - return default_type; - } else { - std::string hash_type_input = param.get_str(); - - if (hash_type_input == "hash_serialized") { - return CoinStatsHashType::HASH_SERIALIZED; - } else if (hash_type_input == "none") { - return CoinStatsHashType::NONE; - } else { - throw JSONRPCError( - RPC_INVALID_PARAMETER, - strprintf("%d is not a valid hash_type", hash_type_input)); - } - } -} - std::string HelpExampleCli(const std::string &methodname, const std::string &args) { return "> bitcoin-cli " + methodname + " " + args + "\n"; diff --git a/test/functional/feature_utxo_set_hash.py b/test/functional/feature_utxo_set_hash.py new file mode 100755 --- /dev/null +++ b/test/functional/feature_utxo_set_hash.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test UTXO set hash value calculation in gettxoutsetinfo.""" + +import struct + +from test_framework.blocktools import create_transaction +from test_framework.messages import CBlock, COutPoint, FromHex +from test_framework.muhash import MuHash3072 +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + + +class UTXOSetHashTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def test_deterministic_hash_results(self): + self.log.info("Test deterministic UTXO set hash results") + + # These depend on the setup_clean_chain option, the chain loaded from + # the cache + assert_equal( + self.nodes[0].gettxoutsetinfo()['hash_serialized'], + "b32ec1dda5a53cd025b95387aad344a801825fe46a60ff952ce26528f01d3be8") + assert_equal( + self.nodes[0].gettxoutsetinfo("muhash")['muhash'], + "dd5ad2a105c2d29495f577245c357409002329b9f4d6182c0af3dc2f462555c8") + + def test_muhash_implementation(self): + self.log.info("Test MuHash implementation consistency") + + node = self.nodes[0] + + # Generate 100 blocks and remove the first since we plan to spend its + # coinbase + block_hashes = node.generate(100) + blocks = list(map( + lambda block: FromHex(CBlock(), node.getblock(block, False)), + block_hashes)) + spending = blocks.pop(0) + + # Create a spending transaction and mine a block which includes it + tx = create_transaction( + node, spending.vtx[0].rehash(), node.getnewaddress(), + amount=49_000_000) + txid = node.sendrawtransaction( + hexstring=tx.serialize().hex(), maxfeerate=0) + + tx_block = node.generateblock( + output=node.getnewaddress(), + transactions=[txid]) + blocks.append( + FromHex(CBlock(), node.getblock(tx_block['hash'], False))) + + # Serialize the outputs that should be in the UTXO set and add them to + # a MuHash object + muhash = MuHash3072() + + for height, block in enumerate(blocks): + # The Genesis block coinbase is not part of the UTXO set and we + # spent the first mined block + height += 2 + + for tx in block.vtx: + for n, tx_out in enumerate(tx.vout): + coinbase = 1 if not tx.vin[0].prevout.hash else 0 + + data = COutPoint(int(tx.rehash(), 16), n).serialize() + data += struct.pack("