|
| 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 <chain.h> |
| 6 | +#include <chainparams.h> |
| 7 | +#include <cstdint> |
| 8 | +#include <flatfile.h> |
| 9 | +#include <test/fuzz/FuzzedDataProvider.h> |
| 10 | +#include <test/fuzz/fuzz.h> |
| 11 | +#include <test/fuzz/util.h> |
| 12 | +#include <test/util/setup_common.h> |
| 13 | + |
| 14 | +#include <optional> |
| 15 | +#include <ranges> |
| 16 | +#include <validation.h> |
| 17 | +#include <vector> |
| 18 | + |
| 19 | +const TestingSetup* g_setup; |
| 20 | + |
| 21 | +CBlockHeader ConsumeBlockHeader(FuzzedDataProvider& provider, uint256 prev_hash, int& nonce_counter) |
| 22 | +{ |
| 23 | + CBlockHeader header; |
| 24 | + header.nVersion = provider.ConsumeIntegral<decltype(header.nVersion)>(); |
| 25 | + header.hashPrevBlock = prev_hash; |
| 26 | + header.hashMerkleRoot = uint256{}; // never used |
| 27 | + header.nTime = provider.ConsumeIntegral<decltype(header.nTime)>(); |
| 28 | + header.nBits = Params().GenesisBlock().nBits; |
| 29 | + header.nNonce = nonce_counter++; // prevent creating multiple block headers with the same hash |
| 30 | + return header; |
| 31 | +} |
| 32 | + |
| 33 | +void initialize_block_index_tree() |
| 34 | +{ |
| 35 | + static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>(); |
| 36 | + g_setup = testing_setup.get(); |
| 37 | +} |
| 38 | + |
| 39 | +FUZZ_TARGET(block_index_tree, .init = initialize_block_index_tree) |
| 40 | +{ |
| 41 | + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); |
| 42 | + ChainstateManager& chainman = *g_setup->m_node.chainman; |
| 43 | + auto& blockman = chainman.m_blockman; |
| 44 | + CBlockIndex* genesis = chainman.ActiveChainstate().m_chain[0]; |
| 45 | + int nonce_counter = 0; |
| 46 | + std::vector<CBlockIndex*> blocks; |
| 47 | + blocks.push_back(genesis); |
| 48 | + |
| 49 | + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 1000) |
| 50 | + { |
| 51 | + CallOneOf( |
| 52 | + fuzzed_data_provider, |
| 53 | + [&] { |
| 54 | + // Receive a header building on an existing one. This assumes headers are valid, so PoW is not relevant here. |
| 55 | + LOCK(cs_main); |
| 56 | + CBlockIndex* prev_block = PickValue(fuzzed_data_provider, blocks); |
| 57 | + if (!(prev_block->nStatus & BLOCK_FAILED_MASK)) { |
| 58 | + CBlockHeader header = ConsumeBlockHeader(fuzzed_data_provider, prev_block->GetBlockHash(), nonce_counter); |
| 59 | + CBlockIndex* index = blockman.AddToBlockIndex(header, chainman.m_best_header); |
| 60 | + assert(index->nStatus & BLOCK_VALID_TREE); |
| 61 | + } |
| 62 | + }, |
| 63 | + [&] { |
| 64 | + // Receive a full block (valid or invalid) for an existing header, but don't attempt to connect it yet |
| 65 | + LOCK(cs_main); |
| 66 | + CBlockIndex* index = PickValue(fuzzed_data_provider, blocks); |
| 67 | + // Must be new to us and not known to be invalid (e.g. because of an invalid ancestor). |
| 68 | + if (index->nTx == 0 && !(index->nStatus & BLOCK_FAILED_MASK)) { |
| 69 | + if (fuzzed_data_provider.ConsumeBool()) { // Invalid |
| 70 | + BlockValidationState state; |
| 71 | + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "consensus-invalid"); |
| 72 | + chainman.ActiveChainstate().InvalidBlockFound(index, state); |
| 73 | + } else { |
| 74 | + size_t nTx = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 1000); |
| 75 | + CBlock block; // Dummy block, so that ReceivedBlockTransaction can infer a nTx value. |
| 76 | + block.vtx = std::vector<CTransactionRef>(nTx); |
| 77 | + FlatFilePos pos(0, fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 1000)); |
| 78 | + chainman.ReceivedBlockTransactions(block, index, pos); |
| 79 | + assert(index->nStatus & BLOCK_VALID_TRANSACTIONS); |
| 80 | + assert(index->nStatus & BLOCK_HAVE_DATA); |
| 81 | + } |
| 82 | + } |
| 83 | + }, |
| 84 | + [&] { |
| 85 | + // Simplified ActivateBestChain(): Try to move to a chain with more work - with the possibility of finding blocks to be invalid on the way |
| 86 | + LOCK(cs_main); |
| 87 | + auto& chain = chainman.ActiveChain(); |
| 88 | + CBlockIndex* old_tip = chain.Tip(); |
| 89 | + assert(old_tip); |
| 90 | + do { |
| 91 | + CBlockIndex* best_tip = chainman.ActiveChainstate().FindMostWorkChain(); |
| 92 | + assert(best_tip); // Should at least return current tip |
| 93 | + if (best_tip == chain.Tip()) break; // Nothing to do |
| 94 | + // Rewind chain to forking point |
| 95 | + const CBlockIndex* fork = chain.FindFork(best_tip); |
| 96 | + // If we can't go back to the fork point due to pruned data, abort and don't do anything. Note that this check does not exist in validation.cpp, where |
| 97 | + // the node would currently just crash in this scenario (although this is very unlikely to happen due to the minimum pruning threshold of 550MiB). |
| 98 | + CBlockIndex* it = chain.Tip(); |
| 99 | + bool pruned_block{false}; |
| 100 | + while (it && it->nHeight != fork->nHeight) { |
| 101 | + if (!(it->nStatus & BLOCK_HAVE_UNDO) && it->nHeight > 0) { |
| 102 | + assert(blockman.m_have_pruned); |
| 103 | + pruned_block = true; |
| 104 | + break; |
| 105 | + } |
| 106 | + it = it->pprev; |
| 107 | + } |
| 108 | + if (pruned_block) break; |
| 109 | + |
| 110 | + chain.SetTip(*chain[fork->nHeight]); |
| 111 | + |
| 112 | + // Prepare new blocks to connect |
| 113 | + std::vector<CBlockIndex*> to_connect; |
| 114 | + it = best_tip; |
| 115 | + while (it && it->nHeight != fork->nHeight) { |
| 116 | + to_connect.push_back(it); |
| 117 | + it = it->pprev; |
| 118 | + } |
| 119 | + // Connect blocks, possibly fail |
| 120 | + for (CBlockIndex* block : to_connect | std::views::reverse) { |
| 121 | + assert(!(block->nStatus & BLOCK_FAILED_MASK)); |
| 122 | + assert(block->nStatus & BLOCK_HAVE_DATA); |
| 123 | + if (!block->IsValid(BLOCK_VALID_SCRIPTS)) { |
| 124 | + if (fuzzed_data_provider.ConsumeBool()) { // Invalid |
| 125 | + BlockValidationState state; |
| 126 | + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "consensus-invalid"); |
| 127 | + chainman.ActiveChainstate().InvalidBlockFound(block, state); |
| 128 | + break; |
| 129 | + } else { |
| 130 | + block->RaiseValidity(BLOCK_VALID_SCRIPTS); |
| 131 | + block->nStatus |= BLOCK_HAVE_UNDO; |
| 132 | + } |
| 133 | + } |
| 134 | + chain.SetTip(*block); |
| 135 | + chainman.ActiveChainstate().PruneBlockIndexCandidates(); |
| 136 | + // ABC may release cs_main / not connect all blocks in one go - but only if we have at least much chain work as we had at the start. |
| 137 | + if (block->nChainWork > old_tip->nChainWork && fuzzed_data_provider.ConsumeBool()) { |
| 138 | + break; |
| 139 | + } |
| 140 | + } |
| 141 | + } while (node::CBlockIndexWorkComparator()(chain.Tip(), old_tip)); |
| 142 | + assert(chain.Tip()->nChainWork >= old_tip->nChainWork); |
| 143 | + }, |
| 144 | + [&] { |
| 145 | + // Prune chain - dealing with block files is beyond the scope of this test, so just prune random blocks, making no assumptions what must |
| 146 | + // be together in a block file. |
| 147 | + // Also don't prune blocks outside of the chain for now - this would make the fuzzer crash because of the problem describted in |
| 148 | + // https://github.com/bitcoin/bitcoin/issues/31512 |
| 149 | + LOCK(cs_main); |
| 150 | + auto& chain = chainman.ActiveChain(); |
| 151 | + int prune_height = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, chain.Height()); |
| 152 | + CBlockIndex* prune_block{chain[prune_height]}; |
| 153 | + if (prune_block != chain.Tip()) { |
| 154 | + blockman.m_have_pruned = true; |
| 155 | + prune_block->nStatus &= ~BLOCK_HAVE_DATA; |
| 156 | + prune_block->nStatus &= ~BLOCK_HAVE_UNDO; |
| 157 | + prune_block->nFile = 0; |
| 158 | + prune_block->nDataPos = 0; |
| 159 | + prune_block->nUndoPos = 0; |
| 160 | + auto range = blockman.m_blocks_unlinked.equal_range(prune_block->pprev); |
| 161 | + while (range.first != range.second) { |
| 162 | + std::multimap<CBlockIndex*, CBlockIndex*>::iterator _it = range.first; |
| 163 | + range.first++; |
| 164 | + if (_it->second == prune_block) { |
| 165 | + blockman.m_blocks_unlinked.erase(_it); |
| 166 | + } |
| 167 | + } |
| 168 | + } |
| 169 | + }); |
| 170 | + } |
| 171 | + chainman.CheckBlockIndex(); |
| 172 | + |
| 173 | + // clean up global state changed by last iteration and prepare for next iteration |
| 174 | + { |
| 175 | + LOCK(cs_main); |
| 176 | + genesis->nStatus |= BLOCK_HAVE_DATA; |
| 177 | + genesis->nStatus |= BLOCK_HAVE_UNDO; |
| 178 | + chainman.m_best_header = genesis; |
| 179 | + chainman.m_best_invalid = nullptr; |
| 180 | + chainman.nBlockSequenceId = 1; |
| 181 | + chainman.ActiveChain().SetTip(*genesis); |
| 182 | + chainman.ActiveChainstate().setBlockIndexCandidates.clear(); |
| 183 | + chainman.m_failed_blocks.clear(); |
| 184 | + blockman.m_blocks_unlinked.clear(); |
| 185 | + blockman.m_have_pruned = false; |
| 186 | + blockman.CleanupForFuzzing(); |
| 187 | + // Delete all blocks but Genesis from block index |
| 188 | + uint256 genesis_hash = genesis->GetBlockHash(); |
| 189 | + for (auto it = blockman.m_block_index.begin(); it != blockman.m_block_index.end();) { |
| 190 | + if (it->first != genesis_hash) { |
| 191 | + it = blockman.m_block_index.erase(it); |
| 192 | + } else { |
| 193 | + ++it; |
| 194 | + } |
| 195 | + } |
| 196 | + chainman.ActiveChainstate().TryAddBlockIndexCandidate(genesis); |
| 197 | + assert(blockman.m_block_index.size() == 1); |
| 198 | + assert(chainman.ActiveChainstate().setBlockIndexCandidates.size() == 1); |
| 199 | + assert(chainman.ActiveChain().Height() == 0); |
| 200 | + } |
| 201 | +} |
0 commit comments