mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-12 21:52:53 +02:00
72db4accbfcoins: drop stale cursor null checks (Lőrinc)3d2f2d8de0coins: pass UTXO stats view by reference (Lőrinc)35aedb2823coins: drop cursor from base view (Lőrinc)c6fbe2f66ccoins: pass DB view to cursor users (Lőrinc) Pull request description: **Problem:** `CCoinsView::Cursor()` makes cursor iteration look like a generic coins view operation, but cursor iteration is only supported by the DB-backed coins view. The cache override only threw, and the `coins_view` fuzz target only asserted that deterministic unsupported throw path. **Fix:** Make cursor iteration a `CCoinsViewDB` operation. Cursor users now take the DB-backed view directly, `CCoinsView` no longer exposes `Cursor()`, and the fuzz target keeps DB-backed cursor coverage while dropping the unsupported cache throw probe. The UTXO stats path is also tightened to pass the non-null DB view by reference, and stale null handling for DB cursors is removed. This was extracted from review discussion in https://github.com/bitcoin/bitcoin/pull/35295#discussion_r3420576781 and extended based on https://github.com/bitcoin/bitcoin/pull/35562#issuecomment-4746585893. ACKs for top commit: achow101: ACK72db4accbfsedited: Re-ACK72db4accbfw0xlt: ACK72db4accbfandrewtoth: ACK72db4accbfTree-SHA512: 12a81330a6ec1b91a7e4393f3761ea9ed4702ecb24312f1defa5a9a079a396ce921fc52f74fe296e5ac7ab20d5b5a8a84e858c96847f333c58b7fa9de9e8143e
192 lines
7.4 KiB
C++
192 lines
7.4 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/amount.h>
|
|
#include <consensus/merkle.h>
|
|
#include <kernel/coinstats.h>
|
|
#include <node/miner.h>
|
|
#include <primitives/block.h>
|
|
#include <primitives/transaction.h>
|
|
#include <script/script.h>
|
|
#include <sync.h>
|
|
#include <test/fuzz/FuzzedDataProvider.h>
|
|
#include <test/fuzz/fuzz.h>
|
|
#include <test/fuzz/util.h>
|
|
#include <test/util/mining.h>
|
|
#include <test/util/random.h>
|
|
#include <test/util/setup_common.h>
|
|
#include <test/util/time.h>
|
|
#include <txdb.h>
|
|
#include <uint256.h>
|
|
#include <util/check.h>
|
|
#include <validation.h>
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
FUZZ_TARGET(utxo_total_supply)
|
|
{
|
|
SeedRandomStateForTest(SeedRand::ZEROS);
|
|
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
|
|
FakeNodeClock clock{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();
|
|
};
|
|
const auto PrepareNextBlock = [&]() {
|
|
// Use OP_FALSE to avoid BIP30 check from hitting early
|
|
auto block = PrepareBlock(node, {
|
|
.coinbase_output_script = CScript() << OP_FALSE,
|
|
});
|
|
// 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);
|
|
// Avoid bad-cb-length error at heights <= 16. Pad the BIP34-encoded height
|
|
// with OP_0 to satisfy the minimum 2-byte coinbase scriptSig length.
|
|
CScript duplicate_coinbase_script = CScript() << duplicate_coinbase_height;
|
|
if (duplicate_coinbase_height <= 16) {
|
|
duplicate_coinbase_script << 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();
|
|
});
|
|
}
|
|
}
|