-
Notifications
You must be signed in to change notification settings - Fork 38.6k
Add hash_type MUHASH for gettxoutsetinfo #19145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a1fccea
2474645
0d3b2f6
6ccc8fc
e987ae5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| #include <node/coinstats.h> | ||
|
|
||
| #include <coins.h> | ||
| #include <crypto/muhash.h> | ||
| #include <hash.h> | ||
| #include <serialize.h> | ||
| #include <uint256.h> | ||
|
|
@@ -24,31 +25,47 @@ static uint64_t GetBogoSize(const CScript& scriptPubKey) | |
| scriptPubKey.size() /* scriptPubKey */; | ||
| } | ||
|
|
||
| static void ApplyStats(CCoinsStats& stats, CHashWriter& ss, const uint256& hash, const std::map<uint32_t, Coin>& outputs) | ||
| static void ApplyHash(CCoinsStats& stats, CHashWriter& ss, const uint256& hash, const std::map<uint32_t, Coin>& outputs, std::map<uint32_t, Coin>::const_iterator it) | ||
fjahr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "refactor: Separate hash and stats calculation in coinstats" (2474645)
Also, passing iterators around like this seems unnecessarily elaborate. It makes sense to separate stats and hash computations, but is there a reason both computations need to share the a single static void ApplyHash(CHashWriter& ss, const uint256& hash, const std::map<uint32_t, Coin>& outputs); |
||
| { | ||
| assert(!outputs.empty()); | ||
| ss << hash; | ||
| ss << VARINT(outputs.begin()->second.nHeight * 2 + outputs.begin()->second.fCoinBase ? 1u : 0u); | ||
| stats.nTransactions++; | ||
| for (const auto& output : outputs) { | ||
| ss << VARINT(output.first + 1); | ||
| ss << output.second.out.scriptPubKey; | ||
| ss << VARINT_MODE(output.second.out.nValue, VarIntMode::NONNEGATIVE_SIGNED); | ||
| stats.nTransactionOutputs++; | ||
| stats.nTotalAmount += output.second.out.nValue; | ||
| stats.nBogoSize += GetBogoSize(output.second.out.scriptPubKey); | ||
| if (it == outputs.begin()) { | ||
| ss << hash; | ||
| ss << VARINT(it->second.nHeight * 2 + it->second.fCoinBase ? 1u : 0u); | ||
| } | ||
|
|
||
| ss << VARINT(it->first + 1); | ||
| ss << it->second.out.scriptPubKey; | ||
| ss << VARINT_MODE(it->second.out.nValue, VarIntMode::NONNEGATIVE_SIGNED); | ||
|
|
||
| if (it == std::prev(outputs.end())) { | ||
| ss << VARINT(0u); | ||
| } | ||
| ss << VARINT(0u); | ||
| } | ||
|
|
||
| static void ApplyStats(CCoinsStats& stats, std::nullptr_t, const uint256& hash, const std::map<uint32_t, Coin>& outputs) | ||
| static void ApplyHash(CCoinsStats& stats, std::nullptr_t, const uint256& hash, const std::map<uint32_t, Coin>& outputs, std::map<uint32_t, Coin>::const_iterator it) {} | ||
|
|
||
| static void ApplyHash(CCoinsStats& stats, MuHash3072& muhash, const uint256& hash, const std::map<uint32_t, Coin>& outputs, std::map<uint32_t, Coin>::const_iterator it) | ||
| { | ||
| COutPoint outpoint = COutPoint(hash, it->first); | ||
| Coin coin = it->second; | ||
|
|
||
| CDataStream ss(SER_DISK, PROTOCOL_VERSION); | ||
| ss << outpoint; | ||
| ss << static_cast<uint32_t>(coin.nHeight * 2 + coin.fCoinBase); | ||
| ss << coin.out; | ||
| muhash.Insert(MakeUCharSpan(ss)); | ||
| } | ||
|
|
||
| template <typename T> | ||
| static void ApplyStats(CCoinsStats& stats, T& hash_obj, const uint256& hash, const std::map<uint32_t, Coin>& outputs) | ||
| { | ||
| assert(!outputs.empty()); | ||
| stats.nTransactions++; | ||
| for (const auto& output : outputs) { | ||
| for (auto it = outputs.begin(); it != outputs.end(); ++it) { | ||
| ApplyHash(stats, hash_obj, hash, outputs, it); | ||
|
|
||
| stats.nTransactionOutputs++; | ||
| stats.nTotalAmount += output.second.out.nValue; | ||
| stats.nBogoSize += GetBogoSize(output.second.out.scriptPubKey); | ||
| stats.nTotalAmount += it->second.out.nValue; | ||
| stats.nBogoSize += GetBogoSize(it->second.out.scriptPubKey); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -104,6 +121,10 @@ bool GetUTXOStats(CCoinsView* view, CCoinsStats& stats, CoinStatsHashType hash_t | |
| CHashWriter ss(SER_GETHASH, PROTOCOL_VERSION); | ||
| return GetUTXOStats(view, stats, ss, interruption_point); | ||
| } | ||
| case(CoinStatsHashType::MUHASH): { | ||
fjahr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| MuHash3072 muhash; | ||
| return GetUTXOStats(view, stats, muhash, interruption_point); | ||
| } | ||
| case(CoinStatsHashType::NONE): { | ||
| return GetUTXOStats(view, stats, nullptr, interruption_point); | ||
| } | ||
|
|
@@ -116,10 +137,18 @@ 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) {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ class CCoinsView; | |
|
|
||
| enum class CoinStatsHashType { | ||
| HASH_SERIALIZED, | ||
| MUHASH, | ||
| NONE, | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1047,13 +1047,26 @@ static RPCHelpMan pruneblockchain() | |
| }; | ||
| } | ||
|
|
||
| CoinStatsHashType ParseHashType(const std::string& hash_type_input) | ||
| { | ||
| if (hash_type_input == "hash_serialized_2") { | ||
| 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", | ||
| "\nReturns statistics about the unspent transaction output set.\n" | ||
| "Note this call may take some time.\n", | ||
| { | ||
| {"hash_type", RPCArg::Type::STR, /* default */ "hash_serialized_2", "Which UTXO set hash should be calculated. Options: 'hash_serialized_2' (the legacy algorithm), 'none'."}, | ||
| {"hash_type", RPCArg::Type::STR, /* default */ "hash_serialized_2", "Which UTXO set hash should be calculated. Options: 'hash_serialized_2' (the legacy algorithm), 'muhash', 'none'."}, | ||
fjahr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, | ||
| RPCResult{ | ||
| RPCResult::Type::OBJ, "", "", | ||
|
|
@@ -1063,7 +1076,8 @@ static RPCHelpMan gettxoutsetinfo() | |
| {RPCResult::Type::NUM, "transactions", "The number of transactions with unspent outputs"}, | ||
| {RPCResult::Type::NUM, "txouts", "The number of unspent transaction outputs"}, | ||
| {RPCResult::Type::NUM, "bogosize", "A meaningless metric for UTXO set size"}, | ||
| {RPCResult::Type::STR_HEX, "hash_serialized_2", "The serialized hash (only present if 'hash_serialized_2' hash_type is chosen)"}, | ||
| {RPCResult::Type::STR_HEX, "hash_serialized_2", /* optional */ true, "The serialized hash (only present if 'hash_serialized_2' hash_type is chosen)"}, | ||
| {RPCResult::Type::STR_HEX, "muhash", /* optional */ true, "The serialized hash (only present if 'muhash' hash_type is chosen)"}, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit, in these two lines, could replace the single quotes with double ones for consistency, e.g.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, will do if I have to retouch! Thanks for re-reviewing! |
||
| {RPCResult::Type::NUM, "disk_size", "The estimated size of the chainstate on disk"}, | ||
| {RPCResult::Type::STR_AMOUNT, "total_amount", "The total amount"}, | ||
| }}, | ||
|
|
@@ -1078,7 +1092,7 @@ static RPCHelpMan gettxoutsetinfo() | |
| 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()); | ||
| NodeContext& node = EnsureNodeContext(request.context); | ||
|
|
@@ -1091,6 +1105,9 @@ static RPCHelpMan gettxoutsetinfo() | |
| if (hash_type == CoinStatsHashType::HASH_SERIALIZED) { | ||
| ret.pushKV("hash_serialized_2", stats.hashSerialized.GetHex()); | ||
| } | ||
| if (hash_type == CoinStatsHashType::MUHASH) { | ||
| ret.pushKV("muhash", stats.hashSerialized.GetHex()); | ||
| } | ||
| ret.pushKV("disk_size", stats.nDiskSize); | ||
| ret.pushKV("total_amount", ValueFromAmount(stats.nTotalAmount)); | ||
| } else { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| #!/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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is no loaded chain and the utxo set is empty. Which is also useful to check, but doesn't check if the hash is deterministic? Did you forget to load the chain from |
||
| assert_equal(self.nodes[0].gettxoutsetinfo()['hash_serialized_2'], "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) | ||
| txid = node.sendrawtransaction(hexstring=tx.serialize_with_witness().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 | ||
|
|
||
| # Skip witness commitment | ||
| if (coinbase and n > 0): | ||
| continue | ||
|
|
||
| data = COutPoint(int(tx.rehash(), 16), n).serialize() | ||
| data += struct.pack("<i", height * 2 + coinbase) | ||
| data += tx_out.serialize() | ||
|
|
||
| muhash.insert(data) | ||
|
|
||
| finalized = muhash.digest() | ||
| node_muhash = node.gettxoutsetinfo("muhash")['muhash'] | ||
|
|
||
| assert_equal(finalized[::-1].hex(), node_muhash) | ||
|
|
||
| def run_test(self): | ||
| self.test_deterministic_hash_results() | ||
| self.test_muhash_implementation() | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| UTXOSetHashTest().main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In commit "refactor: Improve encapsulation between MuHash3072 and Num3072" (a1fccea)
Note in case it helps other reviewers: I found this commit easier to review rearranging the method order to
ToNum3072Num3072ToBytesinstead ofNum3072ToBytesToNum3072so diff was smaller with code moving around less.