Coverage Report

Created: 2025-09-19 18:31

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/root/bitcoin/src/index/coinstatsindex.cpp
Line
Count
Source
1
// Copyright (c) 2020-2022 The Bitcoin Core developers
2
// Distributed under the MIT software license, see the accompanying
3
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5
#include <arith_uint256.h>
6
#include <chainparams.h>
7
#include <coins.h>
8
#include <common/args.h>
9
#include <crypto/muhash.h>
10
#include <index/coinstatsindex.h>
11
#include <kernel/coinstats.h>
12
#include <logging.h>
13
#include <node/blockstorage.h>
14
#include <serialize.h>
15
#include <txdb.h>
16
#include <undo.h>
17
#include <validation.h>
18
19
using kernel::ApplyCoinHash;
20
using kernel::CCoinsStats;
21
using kernel::GetBogoSize;
22
using kernel::RemoveCoinHash;
23
24
static constexpr uint8_t DB_BLOCK_HASH{'s'};
25
static constexpr uint8_t DB_BLOCK_HEIGHT{'t'};
26
static constexpr uint8_t DB_MUHASH{'M'};
27
28
namespace {
29
30
struct DBVal {
31
    uint256 muhash{uint256::ZERO};
32
    uint64_t transaction_output_count{0};
33
    uint64_t bogo_size{0};
34
    CAmount total_amount{0};
35
    CAmount total_subsidy{0};
36
    arith_uint256 total_prevout_spent_amount{0};
37
    arith_uint256 total_new_outputs_ex_coinbase_amount{0};
38
    arith_uint256 total_coinbase_amount{0};
39
    CAmount total_unspendables_genesis_block{0};
40
    CAmount total_unspendables_bip30{0};
41
    CAmount total_unspendables_scripts{0};
42
    CAmount total_unspendables_unclaimed_rewards{0};
43
44
    SERIALIZE_METHODS(DBVal, obj)
45
0
    {
46
0
        uint256 prevout_spent, new_outputs, coinbase;
47
0
        SER_WRITE(obj, prevout_spent = ArithToUint256(obj.total_prevout_spent_amount));
48
0
        SER_WRITE(obj, new_outputs = ArithToUint256(obj.total_new_outputs_ex_coinbase_amount));
49
0
        SER_WRITE(obj, coinbase = ArithToUint256(obj.total_coinbase_amount));
50
51
0
        READWRITE(obj.muhash);
52
0
        READWRITE(obj.transaction_output_count);
53
0
        READWRITE(obj.bogo_size);
54
0
        READWRITE(obj.total_amount);
55
0
        READWRITE(obj.total_subsidy);
56
0
        READWRITE(prevout_spent);
57
0
        READWRITE(new_outputs);
58
0
        READWRITE(coinbase);
59
0
        READWRITE(obj.total_unspendables_genesis_block);
60
0
        READWRITE(obj.total_unspendables_bip30);
61
0
        READWRITE(obj.total_unspendables_scripts);
62
0
        READWRITE(obj.total_unspendables_unclaimed_rewards);
63
64
0
        SER_READ(obj, obj.total_prevout_spent_amount = UintToArith256(prevout_spent));
65
0
        SER_READ(obj, obj.total_new_outputs_ex_coinbase_amount = UintToArith256(new_outputs));
66
0
        SER_READ(obj, obj.total_coinbase_amount = UintToArith256(coinbase));
67
0
    }
Unexecuted instantiation: coinstatsindex.cpp:_ZN12_GLOBAL__N_15DBVal16SerializationOpsI10DataStreamS0_17ActionUnserializeEEvRT0_RT_T1_
Unexecuted instantiation: coinstatsindex.cpp:_ZN12_GLOBAL__N_15DBVal16SerializationOpsI10DataStreamKS0_15ActionSerializeEEvRT0_RT_T1_
68
};
69
70
struct DBHeightKey {
71
    int height;
72
73
0
    explicit DBHeightKey(int height_in) : height(height_in) {}
74
75
    template <typename Stream>
76
    void Serialize(Stream& s) const
77
0
    {
78
0
        ser_writedata8(s, DB_BLOCK_HEIGHT);
79
0
        ser_writedata32be(s, height);
80
0
    }
81
82
    template <typename Stream>
83
    void Unserialize(Stream& s)
84
0
    {
85
0
        const uint8_t prefix{ser_readdata8(s)};
86
0
        if (prefix != DB_BLOCK_HEIGHT) {
87
0
            throw std::ios_base::failure("Invalid format for coinstatsindex DB height key");
88
0
        }
89
0
        height = ser_readdata32be(s);
90
0
    }
91
};
92
93
struct DBHashKey {
94
    uint256 block_hash;
95
96
0
    explicit DBHashKey(const uint256& hash_in) : block_hash(hash_in) {}
97
98
    SERIALIZE_METHODS(DBHashKey, obj)
99
0
    {
100
0
        uint8_t prefix{DB_BLOCK_HASH};
101
0
        READWRITE(prefix);
102
0
        if (prefix != DB_BLOCK_HASH) {
103
0
            throw std::ios_base::failure("Invalid format for coinstatsindex DB hash key");
104
0
        }
105
106
0
        READWRITE(obj.block_hash);
107
0
    }
108
};
109
110
}; // namespace
111
112
std::unique_ptr<CoinStatsIndex> g_coin_stats_index;
113
114
CoinStatsIndex::CoinStatsIndex(std::unique_ptr<interfaces::Chain> chain, size_t n_cache_size, bool f_memory, bool f_wipe)
115
0
    : BaseIndex(std::move(chain), "coinstatsindex")
116
0
{
117
    // An earlier version of the index used "indexes/coinstats" but it contained
118
    // a bug and is superseded by a fixed version at "indexes/coinstatsindex".
119
    // The original index is kept around until the next release in case users
120
    // decide to downgrade their node.
121
0
    auto old_path = gArgs.GetDataDirNet() / "indexes" / "coinstats";
122
0
    if (fs::exists(old_path)) {
123
        // TODO: Change this to deleting the old index with v31.
124
0
        LogWarning("Old version of coinstatsindex found at %s. This folder can be safely deleted unless you " \
125
0
            "plan to downgrade your node to version 29 or lower.", fs::PathToString(old_path));
126
0
    }
127
0
    fs::path path{gArgs.GetDataDirNet() / "indexes" / "coinstatsindex"};
128
0
    fs::create_directories(path);
129
130
0
    m_db = std::make_unique<CoinStatsIndex::DB>(path / "db", n_cache_size, f_memory, f_wipe);
131
0
}
132
133
bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block)
134
0
{
135
0
    const CAmount block_subsidy{GetBlockSubsidy(block.height, Params().GetConsensus())};
136
0
    m_total_subsidy += block_subsidy;
137
138
    // Ignore genesis block
139
0
    if (block.height > 0) {
140
0
        uint256 expected_block_hash{*Assert(block.prev_hash)};
141
0
        if (m_current_block_hash != expected_block_hash) {
142
0
            LogError("previous block header belongs to unexpected block %s; expected %s",
143
0
                      m_current_block_hash.ToString(), expected_block_hash.ToString());
144
0
            return false;
145
0
        }
146
147
        // Add the new utxos created from the block
148
0
        assert(block.data);
149
0
        for (size_t i = 0; i < block.data->vtx.size(); ++i) {
150
0
            const auto& tx{block.data->vtx.at(i)};
151
0
            const bool is_coinbase{tx->IsCoinBase()};
152
153
            // Skip duplicate txid coinbase transactions (BIP30).
154
0
            if (is_coinbase && IsBIP30Unspendable(block.hash, block.height)) {
155
0
                m_total_unspendables_bip30 += block_subsidy;
156
0
                continue;
157
0
            }
158
159
0
            for (uint32_t j = 0; j < tx->vout.size(); ++j) {
160
0
                const CTxOut& out{tx->vout[j]};
161
0
                const Coin coin{out, block.height, is_coinbase};
162
0
                const COutPoint outpoint{tx->GetHash(), j};
163
164
                // Skip unspendable coins
165
0
                if (coin.out.scriptPubKey.IsUnspendable()) {
166
0
                    m_total_unspendables_scripts += coin.out.nValue;
167
0
                    continue;
168
0
                }
169
170
0
                ApplyCoinHash(m_muhash, outpoint, coin);
171
172
0
                if (is_coinbase) {
173
0
                    m_total_coinbase_amount += coin.out.nValue;
174
0
                } else {
175
0
                    m_total_new_outputs_ex_coinbase_amount += coin.out.nValue;
176
0
                }
177
178
0
                ++m_transaction_output_count;
179
0
                m_total_amount += coin.out.nValue;
180
0
                m_bogo_size += GetBogoSize(coin.out.scriptPubKey);
181
0
            }
182
183
            // The coinbase tx has no undo data since no former output is spent
184
0
            if (!is_coinbase) {
185
0
                const auto& tx_undo{Assert(block.undo_data)->vtxundo.at(i - 1)};
186
187
0
                for (size_t j = 0; j < tx_undo.vprevout.size(); ++j) {
188
0
                    const Coin& coin{tx_undo.vprevout[j]};
189
0
                    const COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n};
190
191
0
                    RemoveCoinHash(m_muhash, outpoint, coin);
192
193
0
                    m_total_prevout_spent_amount += coin.out.nValue;
194
195
0
                    --m_transaction_output_count;
196
0
                    m_total_amount -= coin.out.nValue;
197
0
                    m_bogo_size -= GetBogoSize(coin.out.scriptPubKey);
198
0
                }
199
0
            }
200
0
        }
201
0
    } else {
202
        // genesis block
203
0
        m_total_unspendables_genesis_block += block_subsidy;
204
0
    }
205
206
    // If spent prevouts + block subsidy are still a higher amount than
207
    // new outputs + coinbase + current unspendable amount this means
208
    // the miner did not claim the full block reward. Unclaimed block
209
    // rewards are also unspendable.
210
0
    const CAmount temp_total_unspendable_amount{m_total_unspendables_genesis_block + m_total_unspendables_bip30 + m_total_unspendables_scripts + m_total_unspendables_unclaimed_rewards};
211
0
    const arith_uint256 unclaimed_rewards{(m_total_prevout_spent_amount + m_total_subsidy) - (m_total_new_outputs_ex_coinbase_amount + m_total_coinbase_amount + temp_total_unspendable_amount)};
212
0
    assert(unclaimed_rewards <= arith_uint256(std::numeric_limits<CAmount>::max()));
213
0
    m_total_unspendables_unclaimed_rewards += static_cast<CAmount>(unclaimed_rewards.GetLow64());
214
215
0
    std::pair<uint256, DBVal> value;
216
0
    value.first = block.hash;
217
0
    value.second.transaction_output_count = m_transaction_output_count;
218
0
    value.second.bogo_size = m_bogo_size;
219
0
    value.second.total_amount = m_total_amount;
220
0
    value.second.total_subsidy = m_total_subsidy;
221
0
    value.second.total_prevout_spent_amount = m_total_prevout_spent_amount;
222
0
    value.second.total_new_outputs_ex_coinbase_amount = m_total_new_outputs_ex_coinbase_amount;
223
0
    value.second.total_coinbase_amount = m_total_coinbase_amount;
224
0
    value.second.total_unspendables_genesis_block = m_total_unspendables_genesis_block;
225
0
    value.second.total_unspendables_bip30 = m_total_unspendables_bip30;
226
0
    value.second.total_unspendables_scripts = m_total_unspendables_scripts;
227
0
    value.second.total_unspendables_unclaimed_rewards = m_total_unspendables_unclaimed_rewards;
228
229
0
    uint256 out;
230
0
    m_muhash.Finalize(out);
231
0
    value.second.muhash = out;
232
233
0
    m_current_block_hash = block.hash;
234
235
    // Intentionally do not update DB_MUHASH here so it stays in sync with
236
    // DB_BEST_BLOCK, and the index is not corrupted if there is an unclean shutdown.
237
0
    return m_db->Write(DBHeightKey(block.height), value);
238
0
}
239
240
[[nodiscard]] static bool CopyHeightIndexToHashIndex(CDBIterator& db_it, CDBBatch& batch,
241
                                                     const std::string& index_name, int height)
242
0
{
243
0
    DBHeightKey key{height};
244
0
    db_it.Seek(key);
245
246
0
    if (!db_it.GetKey(key) || key.height != height) {
247
0
        LogError("unexpected key in %s: expected (%c, %d)",
248
0
                 index_name, DB_BLOCK_HEIGHT, height);
249
0
        return false;
250
0
    }
251
252
0
    std::pair<uint256, DBVal> value;
253
0
    if (!db_it.GetValue(value)) {
254
0
        LogError("unable to read value in %s at key (%c, %d)",
255
0
                 index_name, DB_BLOCK_HEIGHT, height);
256
0
        return false;
257
0
    }
258
259
0
    batch.Write(DBHashKey(value.first), value.second);
260
0
    return true;
261
0
}
262
263
bool CoinStatsIndex::CustomRemove(const interfaces::BlockInfo& block)
264
0
{
265
0
    CDBBatch batch(*m_db);
266
0
    std::unique_ptr<CDBIterator> db_it(m_db->NewIterator());
267
268
    // During a reorg, copy the block's hash digest from the height index to the hash index,
269
    // ensuring it's still accessible after the height index entry is overwritten.
270
0
    if (!CopyHeightIndexToHashIndex(*db_it, batch, m_name, block.height)) {
271
0
        return false;
272
0
    }
273
274
0
    if (!m_db->WriteBatch(batch)) return false;
275
276
0
    if (!RevertBlock(block)) {
277
0
        return false; // failure cause logged internally
278
0
    }
279
280
0
    return true;
281
0
}
282
283
static bool LookUpOne(const CDBWrapper& db, const interfaces::BlockRef& block, DBVal& result)
284
0
{
285
    // First check if the result is stored under the height index and the value
286
    // there matches the block hash. This should be the case if the block is on
287
    // the active chain.
288
0
    std::pair<uint256, DBVal> read_out;
289
0
    if (!db.Read(DBHeightKey(block.height), read_out)) {
290
0
        return false;
291
0
    }
292
0
    if (read_out.first == block.hash) {
293
0
        result = std::move(read_out.second);
294
0
        return true;
295
0
    }
296
297
    // If value at the height index corresponds to an different block, the
298
    // result will be stored in the hash index.
299
0
    return db.Read(DBHashKey(block.hash), result);
300
0
}
301
302
std::optional<CCoinsStats> CoinStatsIndex::LookUpStats(const CBlockIndex& block_index) const
303
0
{
304
0
    CCoinsStats stats{block_index.nHeight, block_index.GetBlockHash()};
305
0
    stats.index_used = true;
306
307
0
    DBVal entry;
308
0
    if (!LookUpOne(*m_db, {block_index.GetBlockHash(), block_index.nHeight}, entry)) {
309
0
        return std::nullopt;
310
0
    }
311
312
0
    stats.hashSerialized = entry.muhash;
313
0
    stats.nTransactionOutputs = entry.transaction_output_count;
314
0
    stats.nBogoSize = entry.bogo_size;
315
0
    stats.total_amount = entry.total_amount;
316
0
    stats.total_subsidy = entry.total_subsidy;
317
0
    stats.total_prevout_spent_amount = entry.total_prevout_spent_amount;
318
0
    stats.total_new_outputs_ex_coinbase_amount = entry.total_new_outputs_ex_coinbase_amount;
319
0
    stats.total_coinbase_amount = entry.total_coinbase_amount;
320
0
    stats.total_unspendables_genesis_block = entry.total_unspendables_genesis_block;
321
0
    stats.total_unspendables_bip30 = entry.total_unspendables_bip30;
322
0
    stats.total_unspendables_scripts = entry.total_unspendables_scripts;
323
0
    stats.total_unspendables_unclaimed_rewards = entry.total_unspendables_unclaimed_rewards;
324
325
0
    return stats;
326
0
}
327
328
bool CoinStatsIndex::CustomInit(const std::optional<interfaces::BlockRef>& block)
329
0
{
330
0
    if (!m_db->Read(DB_MUHASH, m_muhash)) {
331
        // Check that the cause of the read failure is that the key does not
332
        // exist. Any other errors indicate database corruption or a disk
333
        // failure, and starting the index would cause further corruption.
334
0
        if (m_db->Exists(DB_MUHASH)) {
335
0
            LogError("Cannot read current %s state; index may be corrupted",
336
0
                      GetName());
337
0
            return false;
338
0
        }
339
0
    }
340
341
0
    if (block) {
342
0
        DBVal entry;
343
0
        if (!LookUpOne(*m_db, *block, entry)) {
344
0
            LogError("Cannot read current %s state; index may be corrupted",
345
0
                      GetName());
346
0
            return false;
347
0
        }
348
349
0
        uint256 out;
350
0
        m_muhash.Finalize(out);
351
0
        if (entry.muhash != out) {
352
0
            LogError("Cannot read current %s state; index may be corrupted",
353
0
                      GetName());
354
0
            return false;
355
0
        }
356
357
0
        m_transaction_output_count = entry.transaction_output_count;
358
0
        m_bogo_size = entry.bogo_size;
359
0
        m_total_amount = entry.total_amount;
360
0
        m_total_subsidy = entry.total_subsidy;
361
0
        m_total_prevout_spent_amount = entry.total_prevout_spent_amount;
362
0
        m_total_new_outputs_ex_coinbase_amount = entry.total_new_outputs_ex_coinbase_amount;
363
0
        m_total_coinbase_amount = entry.total_coinbase_amount;
364
0
        m_total_unspendables_genesis_block = entry.total_unspendables_genesis_block;
365
0
        m_total_unspendables_bip30 = entry.total_unspendables_bip30;
366
0
        m_total_unspendables_scripts = entry.total_unspendables_scripts;
367
0
        m_total_unspendables_unclaimed_rewards = entry.total_unspendables_unclaimed_rewards;
368
0
        m_current_block_hash = block->hash;
369
0
    }
370
371
0
    return true;
372
0
}
373
374
bool CoinStatsIndex::CustomCommit(CDBBatch& batch)
375
0
{
376
    // DB_MUHASH should always be committed in a batch together with DB_BEST_BLOCK
377
    // to prevent an inconsistent state of the DB.
378
0
    batch.Write(DB_MUHASH, m_muhash);
379
0
    return true;
380
0
}
381
382
interfaces::Chain::NotifyOptions CoinStatsIndex::CustomOptions()
383
0
{
384
0
    interfaces::Chain::NotifyOptions options;
385
0
    options.connect_undo_data = true;
386
0
    options.disconnect_data = true;
387
0
    options.disconnect_undo_data = true;
388
0
    return options;
389
0
}
390
391
// Revert a single block as part of a reorg
392
bool CoinStatsIndex::RevertBlock(const interfaces::BlockInfo& block)
393
0
{
394
0
    std::pair<uint256, DBVal> read_out;
395
396
    // Ignore genesis block
397
0
    if (block.height > 0) {
398
0
        if (!m_db->Read(DBHeightKey(block.height - 1), read_out)) {
399
0
            return false;
400
0
        }
401
402
0
        uint256 expected_block_hash{*block.prev_hash};
403
0
        if (read_out.first != expected_block_hash) {
404
0
            LogWarning("previous block header belongs to unexpected block %s; expected %s",
405
0
                      read_out.first.ToString(), expected_block_hash.ToString());
406
407
0
            if (!m_db->Read(DBHashKey(expected_block_hash), read_out)) {
408
0
                LogError("previous block header not found; expected %s",
409
0
                          expected_block_hash.ToString());
410
0
                return false;
411
0
            }
412
0
        }
413
0
    }
414
415
    // Roll back muhash by removing the new UTXOs that were created by the
416
    // block and reapplying the old UTXOs that were spent by the block
417
0
    assert(block.data);
418
0
    assert(block.undo_data);
419
0
    for (size_t i = 0; i < block.data->vtx.size(); ++i) {
420
0
        const auto& tx{block.data->vtx.at(i)};
421
0
        const bool is_coinbase{tx->IsCoinBase()};
422
423
0
        if (is_coinbase && IsBIP30Unspendable(block.hash, block.height)) {
424
0
            continue;
425
0
        }
426
427
0
        for (uint32_t j = 0; j < tx->vout.size(); ++j) {
428
0
            const CTxOut& out{tx->vout[j]};
429
0
            const COutPoint outpoint{tx->GetHash(), j};
430
0
            const Coin coin{out, block.height, is_coinbase};
431
432
0
            if (!coin.out.scriptPubKey.IsUnspendable()) {
433
0
                RemoveCoinHash(m_muhash, outpoint, coin);
434
0
            }
435
0
        }
436
437
        // The coinbase tx has no undo data since no former output is spent
438
0
        if (!is_coinbase) {
439
0
            const auto& tx_undo{block.undo_data->vtxundo.at(i - 1)};
440
441
0
            for (size_t j = 0; j < tx_undo.vprevout.size(); ++j) {
442
0
                const Coin& coin{tx_undo.vprevout[j]};
443
0
                const COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n};
444
0
                ApplyCoinHash(m_muhash, outpoint, coin);
445
0
            }
446
0
        }
447
0
    }
448
449
    // Check that the rolled back muhash is consistent with the DB read out
450
0
    uint256 out;
451
0
    m_muhash.Finalize(out);
452
0
    Assert(read_out.second.muhash == out);
453
454
    // Apply the other values from the DB to the member variables
455
0
    m_transaction_output_count = read_out.second.transaction_output_count;
456
0
    m_total_amount = read_out.second.total_amount;
457
0
    m_bogo_size = read_out.second.bogo_size;
458
0
    m_total_subsidy = read_out.second.total_subsidy;
459
0
    m_total_prevout_spent_amount = read_out.second.total_prevout_spent_amount;
460
0
    m_total_new_outputs_ex_coinbase_amount = read_out.second.total_new_outputs_ex_coinbase_amount;
461
0
    m_total_coinbase_amount = read_out.second.total_coinbase_amount;
462
0
    m_total_unspendables_genesis_block = read_out.second.total_unspendables_genesis_block;
463
0
    m_total_unspendables_bip30 = read_out.second.total_unspendables_bip30;
464
0
    m_total_unspendables_scripts = read_out.second.total_unspendables_scripts;
465
0
    m_total_unspendables_unclaimed_rewards = read_out.second.total_unspendables_unclaimed_rewards;
466
0
    m_current_block_hash = *block.prev_hash;
467
468
0
    return true;
469
0
}