bitcoin/src/test/fuzz/utxo_total_supply.cpp
Ava Chow 47c9297172
Merge bitcoin/bitcoin#32420: mining, ipc: omit dummy extraNonce from coinbase
d511adb664edcfb97be44bc0738f49b679240504 [miner] omit dummy extraNonce via IPC (Sjors Provoost)
bf3b5d6d069a0bbb39af0c487fd597257f862f31 test: clarify getCoinbaseRawTx() comparison (Sjors Provoost)
78df9003d63414e4a17b686af7647aeefd706ec5 [doc] Update comments on dummy extraNonces in tests (Anthony Towns)

Pull request description:

  This PR changes the Mining IPC interface to stop including a dummy `extraNonce` in the coinbase `scriptSig` by default, exposing only the consensus-required BIP34 height. This simplifies downstream mining software (including Stratum v2), avoids forcing clients to strip or ignore data we generate, and reduces the risk of incompatibilities if future soft forks add required commitments to the `scriptSig`.

  Existing behavior is preserved for RPCs, tests, regtest, and internal mining by explicitly opting in to the dummy `extraNonce` where needed (e.g. to satisfy `bad-cb-length` at low heights), so consensus rules and test coverage are unchanged. The remainder of the PR consists of small comment fixes, naming clarifications, and test cleanups to make the intent and behavior clearer.

ACKs for top commit:
  achow101:
    ACK d511adb664edcfb97be44bc0738f49b679240504
  ryanofsky:
    Code review ACK d511adb664edcfb97be44bc0738f49b679240504. Just rebased since last review and make suggested tweaks. I'd really like to see this PR merged for the cleanups and sanity it brings to this code. Needs another reviewer though.
  sedited:
    ACK d511adb664edcfb97be44bc0738f49b679240504

Tree-SHA512: d41fa813eb6b5626f4f475d8abc506b29090f4a2d218f2d6824db58b5ebe2ed7c584a903b44de18ccec142bb79c257b0aba6d6da073f56175aec88df96aaaaba
2026-02-02 15:21:16 -08:00

179 lines
7.1 KiB
C++

// Copyright (c) 2020-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <chainparams.h>
#include <consensus/consensus.h>
#include <consensus/merkle.h>
#include <kernel/coinstats.h>
#include <node/miner.h>
#include <script/interpreter.h>
#include <streams.h>
#include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h>
#include <test/fuzz/util.h>
#include <test/util/mining.h>
#include <test/util/setup_common.h>
#include <util/chaintype.h>
#include <util/time.h>
#include <validation.h>
using node::BlockAssembler;
FUZZ_TARGET(utxo_total_supply)
{
SeedRandomStateForTest(SeedRand::ZEROS);
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
SetMockTime(ConsumeTime(fuzzed_data_provider, /*min=*/1296688602)); // regtest genesis block timestamp
/** The testing setup that creates a chainman only (no chainstate) */
ChainTestingSetup test_setup{
ChainType::REGTEST,
{
.extra_args = {
"-testactivationheight=bip34@2",
},
},
};
// Create chainstate
test_setup.LoadVerifyActivateChainstate();
auto& node{test_setup.m_node};
auto& chainman{*Assert(test_setup.m_node.chainman)};
const auto ActiveHeight = [&]() {
LOCK(chainman.GetMutex());
return chainman.ActiveHeight();
};
BlockAssembler::Options options;
options.coinbase_output_script = CScript() << OP_FALSE;
options.include_dummy_extranonce = true;
const auto PrepareNextBlock = [&]() {
// Use OP_FALSE to avoid BIP30 check from hitting early
auto block = PrepareBlock(node, options);
// Replace OP_FALSE with OP_TRUE
{
CMutableTransaction tx{*block->vtx.back()};
tx.nLockTime = 0; // Use the same nLockTime for all as we want to duplicate one of them.
tx.vout.at(0).scriptPubKey = CScript{} << OP_TRUE;
block->vtx.back() = MakeTransactionRef(tx);
}
return block;
};
/** The block template this fuzzer is working on */
auto current_block = PrepareNextBlock();
/** Append-only set of tx outpoints, entries are not removed when spent */
std::vector<std::pair<COutPoint, CTxOut>> txos;
/** The utxo stats at the chain tip */
kernel::CCoinsStats utxo_stats;
/** The total amount of coins in the utxo set */
CAmount circulation{0};
// Store the tx out in the txo map
const auto StoreLastTxo = [&]() {
// get last tx
const CTransaction& tx = *current_block->vtx.back();
// get last out
const uint32_t i = tx.vout.size() - 1;
// store it
txos.emplace_back(COutPoint{tx.GetHash(), i}, tx.vout.at(i));
if (current_block->vtx.size() == 1 && tx.vout.at(i).scriptPubKey[0] == OP_RETURN) {
// also store coinbase
const uint32_t i = tx.vout.size() - 2;
txos.emplace_back(COutPoint{tx.GetHash(), i}, tx.vout.at(i));
}
};
const auto AppendRandomTxo = [&](CMutableTransaction& tx) {
const auto& txo = txos.at(fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, txos.size() - 1));
tx.vin.emplace_back(txo.first);
tx.vout.emplace_back(txo.second.nValue, txo.second.scriptPubKey); // "Forward" coin with no fee
};
const auto UpdateUtxoStats = [&](bool wipe_cache) {
LOCK(chainman.GetMutex());
chainman.ActiveChainstate().ForceFlushStateToDisk(wipe_cache);
utxo_stats = std::move(
*Assert(kernel::ComputeUTXOStats(kernel::CoinStatsHashType::NONE, &chainman.ActiveChainstate().CoinsDB(), chainman.m_blockman, {})));
// Check that miner can't print more money than they are allowed to
assert(circulation == utxo_stats.total_amount);
};
// Update internal state to chain tip
StoreLastTxo();
UpdateUtxoStats(/*wipe_cache=*/fuzzed_data_provider.ConsumeBool());
assert(ActiveHeight() == 0);
// Get at which height we duplicate the coinbase
// Assuming that the fuzzer will mine relatively short chains (less than 200 blocks), we want the duplicate coinbase to be not too high.
// Up to 300 seems reasonable.
int64_t duplicate_coinbase_height = fuzzed_data_provider.ConsumeIntegralInRange(0, 300);
// Always pad with OP_0 at the end to avoid bad-cb-length error
const CScript duplicate_coinbase_script = CScript() << duplicate_coinbase_height << OP_0;
// Mine the first block with this duplicate
current_block = PrepareNextBlock();
StoreLastTxo();
{
// Create duplicate (CScript should match exact format as in CreateNewBlock)
CMutableTransaction tx{*current_block->vtx.front()};
tx.vin.at(0).scriptSig = duplicate_coinbase_script;
// Mine block and create next block template
current_block->vtx.front() = MakeTransactionRef(tx);
}
current_block->hashMerkleRoot = BlockMerkleRoot(*current_block);
assert(!MineBlock(node, current_block).IsNull());
circulation += GetBlockSubsidy(ActiveHeight(), Params().GetConsensus());
assert(ActiveHeight() == 1);
UpdateUtxoStats(/*wipe_cache=*/fuzzed_data_provider.ConsumeBool());
current_block = PrepareNextBlock();
StoreLastTxo();
// Limit to avoid timeout, but enough to cover duplicate_coinbase_height
// and CVE-2018-17144.
LIMITED_WHILE(fuzzed_data_provider.remaining_bytes(), 2'00)
{
CallOneOf(
fuzzed_data_provider,
[&] {
// Append an input-output pair to the last tx in the current block
CMutableTransaction tx{*current_block->vtx.back()};
AppendRandomTxo(tx);
current_block->vtx.back() = MakeTransactionRef(tx);
StoreLastTxo();
},
[&] {
// Append a tx to the list of txs in the current block
CMutableTransaction tx{};
AppendRandomTxo(tx);
current_block->vtx.push_back(MakeTransactionRef(tx));
StoreLastTxo();
},
[&] {
// Append the current block to the active chain
node::RegenerateCommitments(*current_block, chainman);
const bool was_valid = !MineBlock(node, current_block).IsNull();
const uint256 prev_hash_serialized{utxo_stats.hashSerialized};
if (was_valid) {
if (duplicate_coinbase_height == ActiveHeight()) {
// we mined the duplicate coinbase
assert(current_block->vtx.at(0)->vin.at(0).scriptSig == duplicate_coinbase_script);
}
circulation += GetBlockSubsidy(ActiveHeight(), Params().GetConsensus());
}
UpdateUtxoStats(/*wipe_cache=*/fuzzed_data_provider.ConsumeBool());
if (!was_valid) {
// utxo stats must not change
assert(prev_hash_serialized == utxo_stats.hashSerialized);
}
current_block = PrepareNextBlock();
StoreLastTxo();
});
}
}