Repository: bitcoin/bitcoin
File: src/index/coinstatsindex.cpp (C++)

Code before:
#include <node/blockstorage.h>
#include <serialize.h>
#include <txdb.h>
#include <undo.h>
#include <validation.h>

using kernel::ApplyCoinHash;
using kernel::CCoinsStats;
using kernel::GetBogoSize;
using kernel::RemoveCoinHash;

static constexpr uint8_t DB_BLOCK_HASH{'s'};
static constexpr uint8_t DB_BLOCK_HEIGHT{'t'};
static constexpr uint8_t DB_MUHASH{'M'};

namespace {

struct DBVal {
    uint256 muhash;
    uint64_t transaction_output_count;
    uint64_t bogo_size;
    CAmount total_amount;
    CAmount total_subsidy;
    CAmount total_unspendable_amount;
    CAmount total_prevout_spent_amount;
    CAmount total_new_outputs_ex_coinbase_amount;
    CAmount total_coinbase_amount;
    CAmount total_unspendables_genesis_block;
    CAmount total_unspendables_bip30;
    CAmount total_unspendables_scripts;
    CAmount total_unspendables_unclaimed_rewards;

    SERIALIZE_METHODS(DBVal, obj)
    {
        READWRITE(obj.muhash);
        READWRITE(obj.transaction_output_count);
        READWRITE(obj.bogo_size);
        READWRITE(obj.total_amount);
        READWRITE(obj.total_subsidy);
        READWRITE(obj.total_unspendable_amount);
        READWRITE(obj.total_prevout_spent_amount);
        READWRITE(obj.total_new_outputs_ex_coinbase_amount);
        READWRITE(obj.total_coinbase_amount);
        READWRITE(obj.total_unspendables_genesis_block);
        READWRITE(obj.total_unspendables_bip30);
        READWRITE(obj.total_unspendables_scripts);
        READWRITE(obj.total_unspendables_unclaimed_rewards);
    }
};

struct DBHeightKey {

Review comment (bug):
Both `prevout_spent_amount` and `new_outputs_ex_coinbase_amount` can overflow an int64_t in extreme cases -- filling a block with 15000 65-byte transactions (975kvB) repeatedly spending the same 7M BTC will hit 105 billion BTC, which is 1.05e19 sats which is about 13% more than 2**63-1 which is the max value a CAmount can store, and this is undefined behaviour in C++. (You'd need to cycle spends of ~12M BTC or more to overflow a uint64_t, though that is not undefined behaviour)

Even if we don't want to actually fix this now because it's implausible for mainnet and would require a deliberate attack even on test nets (and is not even possible on regtest, as the halving schedule means total supply is only 15k rtBTC), having a dummy overflow field for these two values that's always set to zero would make it possible to fix this in future without having to reindex the entire blockchain.

Code after:
#include <logging.h>
#include <node/blockstorage.h>
#include <serialize.h>
#include <txdb.h>
#include <undo.h>
#include <validation.h>

using kernel::ApplyCoinHash;
using kernel::CCoinsStats;
using kernel::GetBogoSize;
using kernel::RemoveCoinHash;

static constexpr uint8_t DB_BLOCK_HASH{'s'};
static constexpr uint8_t DB_BLOCK_HEIGHT{'t'};
static constexpr uint8_t DB_MUHASH{'M'};

namespace {

struct DBVal {
    uint256 muhash{uint256::ZERO};
    uint64_t transaction_output_count{0};
    uint64_t bogo_size{0};
    CAmount total_amount{0};
    CAmount total_subsidy{0};
    arith_uint256 total_prevout_spent_amount{0};
    arith_uint256 total_new_outputs_ex_coinbase_amount{0};
    arith_uint256 total_coinbase_amount{0};
    CAmount total_unspendables_genesis_block{0};
    CAmount total_unspendables_bip30{0};
    CAmount total_unspendables_scripts{0};
    CAmount total_unspendables_unclaimed_rewards{0};

    SERIALIZE_METHODS(DBVal, obj)
    {
        uint256 prevout_spent, new_outputs, coinbase;
        SER_WRITE(obj, prevout_spent = ArithToUint256(obj.total_prevout_spent_amount));
        SER_WRITE(obj, new_outputs = ArithToUint256(obj.total_new_outputs_ex_coinbase_amount));
        SER_WRITE(obj, coinbase = ArithToUint256(obj.total_coinbase_amount));

        READWRITE(obj.muhash);
        READWRITE(obj.transaction_output_count);
        READWRITE(obj.bogo_size);
        READWRITE(obj.total_amount);
        READWRITE(obj.total_subsidy);
        READWRITE(prevout_spent);
        READWRITE(new_outputs);
        READWRITE(coinbase);
        READWRITE(obj.total_unspendables_genesis_block);
        READWRITE(obj.total_unspendables_bip30);
        READWRITE(obj.total_unspendables_scripts);
        READWRITE(obj.total_unspendables_unclaimed_rewards);