Skip to content

Commit 351370a

Browse files
fjahrtheStack
andcommitted
coinstats: Fix hash_serialized2 calculation
The legacy serialization was vulnerable to maleation and is fixed by adopting the same serialization procedure as was already in use for MuHash. This also includes necessary test fixes where the hash_serialized2 was hardcoded as well as correction of the regtest chainparams. Co-authored-by: Sebastian Falbesoner <[email protected]>
1 parent c1106cf commit 351370a

8 files changed

+48
-53
lines changed

src/index/coinstatsindex.cpp

+6-5
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
#include <undo.h>
1616
#include <validation.h>
1717

18+
using kernel::ApplyCoinHash;
1819
using kernel::CCoinsStats;
1920
using kernel::GetBogoSize;
20-
using kernel::TxOutSer;
21+
using kernel::RemoveCoinHash;
2122

2223
static constexpr uint8_t DB_BLOCK_HASH{'s'};
2324
static constexpr uint8_t DB_BLOCK_HEIGHT{'t'};
@@ -166,7 +167,7 @@ bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block)
166167
continue;
167168
}
168169

169-
m_muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin)));
170+
ApplyCoinHash(m_muhash, outpoint, coin);
170171

171172
if (tx->IsCoinBase()) {
172173
m_total_coinbase_amount += coin.out.nValue;
@@ -187,7 +188,7 @@ bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block)
187188
Coin coin{tx_undo.vprevout[j]};
188189
COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n};
189190

190-
m_muhash.Remove(MakeUCharSpan(TxOutSer(outpoint, coin)));
191+
RemoveCoinHash(m_muhash, outpoint, coin);
191192

192193
m_total_prevout_spent_amount += coin.out.nValue;
193194

@@ -443,7 +444,7 @@ bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex
443444
continue;
444445
}
445446

446-
m_muhash.Remove(MakeUCharSpan(TxOutSer(outpoint, coin)));
447+
RemoveCoinHash(m_muhash, outpoint, coin);
447448

448449
if (tx->IsCoinBase()) {
449450
m_total_coinbase_amount -= coin.out.nValue;
@@ -464,7 +465,7 @@ bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex
464465
Coin coin{tx_undo.vprevout[j]};
465466
COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n};
466467

467-
m_muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin)));
468+
ApplyCoinHash(m_muhash, outpoint, coin);
468469

469470
m_total_prevout_spent_amount -= coin.out.nValue;
470471

src/kernel/chainparams.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -494,14 +494,14 @@ class CRegTestParams : public CChainParams
494494
m_assumeutxo_data = {
495495
{
496496
.height = 110,
497-
.hash_serialized = AssumeutxoHash{uint256S("0x1ebbf5850204c0bdb15bf030f47c7fe91d45c44c712697e4509ba67adb01c618")},
497+
.hash_serialized = AssumeutxoHash{uint256S("0x6657b736d4fe4db0cbc796789e812d5dba7f5c143764b1b6905612f1830609d1")},
498498
.nChainTx = 111,
499499
.blockhash = uint256S("0x696e92821f65549c7ee134edceeeeaaa4105647a3c4fd9f298c0aec0ab50425c")
500500
},
501501
{
502502
// For use by test/functional/feature_assumeutxo.py
503503
.height = 299,
504-
.hash_serialized = AssumeutxoHash{uint256S("0xef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0")},
504+
.hash_serialized = AssumeutxoHash{uint256S("0x61d9c2b29a2571a5fe285fe2d8554f91f93309666fc9b8223ee96338de25ff53")},
505505
.nChainTx = 300,
506506
.blockhash = uint256S("0x7e0517ef3ea6ecbed9117858e42eedc8eb39e8698a38dcbd1b3962a283233f4c")
507507
},

src/kernel/coinstats.cpp

+27-37
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,35 @@ uint64_t GetBogoSize(const CScript& script_pub_key)
4848
script_pub_key.size() /* scriptPubKey */;
4949
}
5050

51-
DataStream TxOutSer(const COutPoint& outpoint, const Coin& coin)
51+
template <typename T>
52+
static void TxOutSer(T& ss, const COutPoint& outpoint, const Coin& coin)
5253
{
53-
DataStream ss{};
5454
ss << outpoint;
55-
ss << static_cast<uint32_t>(coin.nHeight * 2 + coin.fCoinBase);
55+
ss << static_cast<uint32_t>((coin.nHeight << 1) + coin.fCoinBase);
5656
ss << coin.out;
57-
return ss;
5857
}
5958

59+
static void ApplyCoinHash(HashWriter& ss, const COutPoint& outpoint, const Coin& coin)
60+
{
61+
TxOutSer(ss, outpoint, coin);
62+
}
63+
64+
void ApplyCoinHash(MuHash3072& muhash, const COutPoint& outpoint, const Coin& coin)
65+
{
66+
DataStream ss{};
67+
TxOutSer(ss, outpoint, coin);
68+
muhash.Insert(MakeUCharSpan(ss));
69+
}
70+
71+
void RemoveCoinHash(MuHash3072& muhash, const COutPoint& outpoint, const Coin& coin)
72+
{
73+
DataStream ss{};
74+
TxOutSer(ss, outpoint, coin);
75+
muhash.Remove(MakeUCharSpan(ss));
76+
}
77+
78+
static void ApplyCoinHash(std::nullptr_t, const COutPoint& outpoint, const Coin& coin) {}
79+
6080
//! Warning: be very careful when changing this! assumeutxo and UTXO snapshot
6181
//! validation commitments are reliant on the hash constructed by this
6282
//! function.
@@ -69,32 +89,13 @@ DataStream TxOutSer(const COutPoint& outpoint, const Coin& coin)
6989
//! It is also possible, though very unlikely, that a change in this
7090
//! construction could cause a previously invalid (and potentially malicious)
7191
//! UTXO snapshot to be considered valid.
72-
static void ApplyHash(HashWriter& ss, const uint256& hash, const std::map<uint32_t, Coin>& outputs)
73-
{
74-
for (auto it = outputs.begin(); it != outputs.end(); ++it) {
75-
if (it == outputs.begin()) {
76-
ss << hash;
77-
ss << VARINT(it->second.nHeight * 2 + it->second.fCoinBase ? 1u : 0u);
78-
}
79-
80-
ss << VARINT(it->first + 1);
81-
ss << it->second.out.scriptPubKey;
82-
ss << VARINT_MODE(it->second.out.nValue, VarIntMode::NONNEGATIVE_SIGNED);
83-
84-
if (it == std::prev(outputs.end())) {
85-
ss << VARINT(0u);
86-
}
87-
}
88-
}
89-
90-
static void ApplyHash(std::nullptr_t, const uint256& hash, const std::map<uint32_t, Coin>& outputs) {}
91-
92-
static void ApplyHash(MuHash3072& muhash, const uint256& hash, const std::map<uint32_t, Coin>& outputs)
92+
template <typename T>
93+
static void ApplyHash(T& hash_obj, const uint256& hash, const std::map<uint32_t, Coin>& outputs)
9394
{
9495
for (auto it = outputs.begin(); it != outputs.end(); ++it) {
9596
COutPoint outpoint = COutPoint(hash, it->first);
9697
Coin coin = it->second;
97-
muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin)));
98+
ApplyCoinHash(hash_obj, outpoint, coin);
9899
}
99100
}
100101

@@ -118,8 +119,6 @@ static bool ComputeUTXOStats(CCoinsView* view, CCoinsStats& stats, T hash_obj, c
118119
std::unique_ptr<CCoinsViewCursor> pcursor(view->Cursor());
119120
assert(pcursor);
120121

121-
PrepareHash(hash_obj, stats);
122-
123122
uint256 prevkey;
124123
std::map<uint32_t, Coin> outputs;
125124
while (pcursor->Valid()) {
@@ -180,15 +179,6 @@ std::optional<CCoinsStats> ComputeUTXOStats(CoinStatsHashType hash_type, CCoinsV
180179
return stats;
181180
}
182181

183-
// The legacy hash serializes the hashBlock
184-
static void PrepareHash(HashWriter& ss, const CCoinsStats& stats)
185-
{
186-
ss << stats.hashBlock;
187-
}
188-
// MuHash does not need the prepare step
189-
static void PrepareHash(MuHash3072& muhash, CCoinsStats& stats) {}
190-
static void PrepareHash(std::nullptr_t, CCoinsStats& stats) {}
191-
192182
static void FinalizeHash(HashWriter& ss, CCoinsStats& stats)
193183
{
194184
stats.hashSerialized = ss.GetHash();

src/kernel/coinstats.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#define BITCOIN_KERNEL_COINSTATS_H
77

88
#include <consensus/amount.h>
9+
#include <crypto/muhash.h>
910
#include <streams.h>
1011
#include <uint256.h>
1112

@@ -72,7 +73,8 @@ struct CCoinsStats {
7273

7374
uint64_t GetBogoSize(const CScript& script_pub_key);
7475

75-
DataStream TxOutSer(const COutPoint& outpoint, const Coin& coin);
76+
void ApplyCoinHash(MuHash3072& muhash, const COutPoint& outpoint, const Coin& coin);
77+
void RemoveCoinHash(MuHash3072& muhash, const COutPoint& outpoint, const Coin& coin);
7678

7779
std::optional<CCoinsStats> ComputeUTXOStats(CoinStatsHashType hash_type, CCoinsView* view, node::BlockManager& blockman, const std::function<void()>& interruption_point = {});
7880
} // namespace kernel

src/test/validation_tests.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,11 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo)
137137
}
138138

139139
const auto out110 = *params->AssumeutxoForHeight(110);
140-
BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "1ebbf5850204c0bdb15bf030f47c7fe91d45c44c712697e4509ba67adb01c618");
140+
BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "6657b736d4fe4db0cbc796789e812d5dba7f5c143764b1b6905612f1830609d1");
141141
BOOST_CHECK_EQUAL(out110.nChainTx, 111U);
142142

143143
const auto out110_2 = *params->AssumeutxoForBlockhash(uint256S("0x696e92821f65549c7ee134edceeeeaaa4105647a3c4fd9f298c0aec0ab50425c"));
144-
BOOST_CHECK_EQUAL(out110_2.hash_serialized.ToString(), "1ebbf5850204c0bdb15bf030f47c7fe91d45c44c712697e4509ba67adb01c618");
144+
BOOST_CHECK_EQUAL(out110_2.hash_serialized.ToString(), "6657b736d4fe4db0cbc796789e812d5dba7f5c143764b1b6905612f1830609d1");
145145
BOOST_CHECK_EQUAL(out110_2.nChainTx, 111U);
146146
}
147147

test/functional/feature_assumeutxo.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,18 @@ def expected_error(log_msg="", rpc_details=""):
9494

9595
self.log.info(" - snapshot file with alternated UTXO data")
9696
cases = [
97-
[b"\xff" * 32, 0, "29926acf3ac81f908cf4f22515713ca541c08bb0f0ef1b2c3443a007134d69b8"], # wrong outpoint hash
98-
[(1).to_bytes(4, "little"), 32, "798266c2e1f9a98fe5ce61f5951cbf47130743f3764cf3cbc254be129142cf9d"], # wrong outpoint index
97+
[b"\xff" * 32, 0, "05030e506678f2eca8d624ffed97090ab3beadad1b51ee6e5985ba91c5720e37"], # wrong outpoint hash
98+
[(1).to_bytes(4, "little"), 32, "7d29cfe2c1e242bc6f103878bb70cfffa8b4dac20dbd001ff6ce24b7de2d2399"], # wrong outpoint index
99+
[b"\x81", 36, "f03939a195531f96d5dff983e294a1af62af86049fa7a19a7627246f237c03f1"], # wrong coin code VARINT((coinbase ? 1 : 0) | (height << 1))
100+
[b"\x83", 36, "e4577da84590fb288c0f7967e89575e1b0aa46624669640f6f5dfef028d39930"], # another wrong coin code
99101
]
100102

101103
for content, offset, wrong_hash in cases:
102104
with open(bad_snapshot_path, "wb") as f:
103105
f.write(valid_snapshot_contents[:(32 + 8 + offset)])
104106
f.write(content)
105107
f.write(valid_snapshot_contents[(32 + 8 + offset + len(content)):])
106-
expected_error(log_msg=f"[snapshot] bad snapshot content hash: expected ef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0, got {wrong_hash}")
108+
expected_error(log_msg=f"[snapshot] bad snapshot content hash: expected 61d9c2b29a2571a5fe285fe2d8554f91f93309666fc9b8223ee96338de25ff53, got {wrong_hash}")
107109

108110
def run_test(self):
109111
"""
@@ -150,7 +152,7 @@ def run_test(self):
150152

151153
assert_equal(
152154
dump_output['txoutset_hash'],
153-
'ef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0')
155+
'61d9c2b29a2571a5fe285fe2d8554f91f93309666fc9b8223ee96338de25ff53')
154156
assert_equal(dump_output['nchaintx'], 300)
155157
assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
156158

test/functional/feature_utxo_set_hash.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def test_muhash_implementation(self):
6969
assert_equal(finalized[::-1].hex(), node_muhash)
7070

7171
self.log.info("Test deterministic UTXO set hash results")
72-
assert_equal(node.gettxoutsetinfo()['hash_serialized_2'], "f9aa4fb5ffd10489b9a6994e70ccf1de8a8bfa2d5f201d9857332e9954b0855d")
72+
assert_equal(node.gettxoutsetinfo()['hash_serialized_2'], "d1c7fec1c0623f6793839878cbe2a531eb968b50b27edd6e2a57077a5aed6094")
7373
assert_equal(node.gettxoutsetinfo("muhash")['muhash'], "d1725b2fe3ef43e55aa4907480aea98d406fc9e0bf8f60169e2305f1fbf5961b")
7474

7575
def run_test(self):

test/functional/rpc_dumptxoutset.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def run_test(self):
4646
'b1bacb602eacf5fbc9a7c2ef6eeb0d229c04e98bdf0c2ea5929012cd0eae3830')
4747

4848
assert_equal(
49-
out['txoutset_hash'], '1f7e3befd45dc13ae198dfbb22869a9c5c4196f8e9ef9735831af1288033f890')
49+
out['txoutset_hash'], 'a0b7baa3bf5ccbd3279728f230d7ca0c44a76e9923fca8f32dbfd08d65ea496a')
5050
assert_equal(out['nchaintx'], 101)
5151

5252
# Specifying a path to an existing or invalid file will fail.

0 commit comments

Comments
 (0)