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("