From 7dd613b26870d628d84787b2f8a1c745931f97a6 Mon Sep 17 00:00:00 2001 From: Martin Zumsande Date: Wed, 30 Oct 2024 14:59:37 -0400 Subject: [PATCH] fuzz: Add fuzzer for block index This fuzz target creates arbitrary tree-like structure of indices, simulating the following events: - Adding a header to the block tree db - Receiving the full block (may be valid or not) - Reorging to a new chain tip (possibly encountering invalid blocks on the way) - pruning The test skips all actual validation of header/ block / transaction data by just simulating the outcome, and also doesn't interact with the data directory. The main goal is to test the integrity of the block index tree in all fuzzed constellations, by calling CheckBlockIndex() at the end of each iteration. --- src/node/blockstorage.cpp | 7 + src/node/blockstorage.h | 2 + src/test/fuzz/CMakeLists.txt | 1 + src/test/fuzz/block_index_tree.cpp | 204 +++++++++++++++++++++++++++++ src/validation.h | 6 +- 5 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 src/test/fuzz/block_index_tree.cpp diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 372395dd24d..8af85ae7c31 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -1164,6 +1164,13 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts) return std::vector{xor_key.begin(), xor_key.end()}; } +void BlockManager::CleanupForFuzzing() +{ + m_dirty_blockindex.clear(); + m_dirty_fileinfo.clear(); + m_blockfile_info.resize(1); +} + BlockManager::BlockManager(const util::SignalInterrupt& interrupt, Options opts) : m_prune_mode{opts.prune_target > 0}, m_xor_key{InitBlocksdirXorKey(opts)}, diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index 8a34efadfea..3a5fd2a2938 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -421,6 +421,8 @@ public: bool ReadBlockUndo(CBlockUndo& blockundo, const CBlockIndex& index) const; void CleanupBlockRevFiles() const; + /** Clear internal state (test-only, only for fuzzing) **/ + void CleanupForFuzzing(); }; // Calls ActivateBestChain() even if no blocks are imported. diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt index a261d3ecea2..27d14c80b7e 100644 --- a/src/test/fuzz/CMakeLists.txt +++ b/src/test/fuzz/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(fuzz block.cpp block_header.cpp block_index.cpp + block_index_tree.cpp blockfilter.cpp bloom_filter.cpp buffered_file.cpp diff --git a/src/test/fuzz/block_index_tree.cpp b/src/test/fuzz/block_index_tree.cpp new file mode 100644 index 00000000000..0ced967cf74 --- /dev/null +++ b/src/test/fuzz/block_index_tree.cpp @@ -0,0 +1,204 @@ +// Copyright (c) 2020-2022 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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +const TestingSetup* g_setup; + +CBlockHeader ConsumeBlockHeader(FuzzedDataProvider& provider, uint256 prev_hash, int& nonce_counter) +{ + CBlockHeader header; + header.nVersion = provider.ConsumeIntegral(); + header.hashPrevBlock = prev_hash; + header.hashMerkleRoot = uint256{}; // never used + header.nTime = provider.ConsumeIntegral(); + header.nBits = Params().GenesisBlock().nBits; + header.nNonce = nonce_counter++; // prevent creating multiple block headers with the same hash + return header; +} + +void initialize_block_index_tree() +{ + static const auto testing_setup = MakeNoLogFileContext(); + g_setup = testing_setup.get(); +} + +FUZZ_TARGET(block_index_tree, .init = initialize_block_index_tree) +{ + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + SetMockTime(ConsumeTime(fuzzed_data_provider)); + ChainstateManager& chainman = *g_setup->m_node.chainman; + auto& blockman = chainman.m_blockman; + CBlockIndex* genesis = chainman.ActiveChainstate().m_chain[0]; + int nonce_counter = 0; + std::vector blocks; + blocks.push_back(genesis); + + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 1000) + { + CallOneOf( + fuzzed_data_provider, + [&] { + // Receive a header building on an existing one. This assumes headers are valid, so PoW is not relevant here. + LOCK(cs_main); + CBlockIndex* prev_block = PickValue(fuzzed_data_provider, blocks); + if (!(prev_block->nStatus & BLOCK_FAILED_MASK)) { + CBlockHeader header = ConsumeBlockHeader(fuzzed_data_provider, prev_block->GetBlockHash(), nonce_counter); + CBlockIndex* index = blockman.AddToBlockIndex(header, chainman.m_best_header); + assert(index->nStatus & BLOCK_VALID_TREE); + blocks.push_back(index); + } + }, + [&] { + // Receive a full block (valid or invalid) for an existing header, but don't attempt to connect it yet + LOCK(cs_main); + CBlockIndex* index = PickValue(fuzzed_data_provider, blocks); + // Must be new to us and not known to be invalid (e.g. because of an invalid ancestor). + if (index->nTx == 0 && !(index->nStatus & BLOCK_FAILED_MASK)) { + if (fuzzed_data_provider.ConsumeBool()) { // Invalid + BlockValidationState state; + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "consensus-invalid"); + chainman.ActiveChainstate().InvalidBlockFound(index, state); + } else { + size_t nTx = fuzzed_data_provider.ConsumeIntegralInRange(1, 1000); + CBlock block; // Dummy block, so that ReceivedBlockTransaction can infer a nTx value. + block.vtx = std::vector(nTx); + FlatFilePos pos(0, fuzzed_data_provider.ConsumeIntegralInRange(1, 1000)); + chainman.ReceivedBlockTransactions(block, index, pos); + assert(index->nStatus & BLOCK_VALID_TRANSACTIONS); + assert(index->nStatus & BLOCK_HAVE_DATA); + } + } + }, + [&] { + // Simplified ActivateBestChain(): Try to move to a chain with more work - with the possibility of finding blocks to be invalid on the way + LOCK(cs_main); + auto& chain = chainman.ActiveChain(); + CBlockIndex* old_tip = chain.Tip(); + assert(old_tip); + do { + CBlockIndex* best_tip = chainman.ActiveChainstate().FindMostWorkChain(); + assert(best_tip); // Should at least return current tip + if (best_tip == chain.Tip()) break; // Nothing to do + // Rewind chain to forking point + const CBlockIndex* fork = chain.FindFork(best_tip); + // 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 + // the node would currently just crash in this scenario (although this is very unlikely to happen due to the minimum pruning threshold of 550MiB). + CBlockIndex* it = chain.Tip(); + bool pruned_block{false}; + while (it && it->nHeight != fork->nHeight) { + if (!(it->nStatus & BLOCK_HAVE_UNDO) && it->nHeight > 0) { + assert(blockman.m_have_pruned); + pruned_block = true; + break; + } + it = it->pprev; + } + if (pruned_block) break; + + chain.SetTip(*chain[fork->nHeight]); + + // Prepare new blocks to connect + std::vector to_connect; + it = best_tip; + while (it && it->nHeight != fork->nHeight) { + to_connect.push_back(it); + it = it->pprev; + } + // Connect blocks, possibly fail + for (CBlockIndex* block : to_connect | std::views::reverse) { + assert(!(block->nStatus & BLOCK_FAILED_MASK)); + assert(block->nStatus & BLOCK_HAVE_DATA); + if (!block->IsValid(BLOCK_VALID_SCRIPTS)) { + if (fuzzed_data_provider.ConsumeBool()) { // Invalid + BlockValidationState state; + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "consensus-invalid"); + chainman.ActiveChainstate().InvalidBlockFound(block, state); + break; + } else { + block->RaiseValidity(BLOCK_VALID_SCRIPTS); + block->nStatus |= BLOCK_HAVE_UNDO; + } + } + chain.SetTip(*block); + chainman.ActiveChainstate().PruneBlockIndexCandidates(); + // 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. + if (block->nChainWork > old_tip->nChainWork && fuzzed_data_provider.ConsumeBool()) { + break; + } + } + } while (node::CBlockIndexWorkComparator()(chain.Tip(), old_tip)); + assert(chain.Tip()->nChainWork >= old_tip->nChainWork); + }, + [&] { + // Prune chain - dealing with block files is beyond the scope of this test, so just prune random blocks, making no assumptions what must + // be together in a block file. + // Also don't prune blocks outside of the chain for now - this would make the fuzzer crash because of the problem describted in + // https://github.com/bitcoin/bitcoin/issues/31512 + LOCK(cs_main); + auto& chain = chainman.ActiveChain(); + int prune_height = fuzzed_data_provider.ConsumeIntegralInRange(0, chain.Height()); + CBlockIndex* prune_block{chain[prune_height]}; + if (prune_block != chain.Tip()) { + blockman.m_have_pruned = true; + prune_block->nStatus &= ~BLOCK_HAVE_DATA; + prune_block->nStatus &= ~BLOCK_HAVE_UNDO; + prune_block->nFile = 0; + prune_block->nDataPos = 0; + prune_block->nUndoPos = 0; + auto range = blockman.m_blocks_unlinked.equal_range(prune_block->pprev); + while (range.first != range.second) { + std::multimap::iterator _it = range.first; + range.first++; + if (_it->second == prune_block) { + blockman.m_blocks_unlinked.erase(_it); + } + } + } + }); + } + chainman.CheckBlockIndex(); + + // clean up global state changed by last iteration and prepare for next iteration + { + LOCK(cs_main); + blocks.clear(); + genesis->nStatus |= BLOCK_HAVE_DATA; + genesis->nStatus |= BLOCK_HAVE_UNDO; + chainman.m_best_header = genesis; + chainman.m_best_invalid = nullptr; + chainman.nBlockSequenceId = 1; + chainman.ActiveChain().SetTip(*genesis); + chainman.ActiveChainstate().setBlockIndexCandidates.clear(); + chainman.m_failed_blocks.clear(); + blockman.m_blocks_unlinked.clear(); + blockman.m_have_pruned = false; + blockman.CleanupForFuzzing(); + // Delete all blocks but Genesis from block index + uint256 genesis_hash = genesis->GetBlockHash(); + for (auto it = blockman.m_block_index.begin(); it != blockman.m_block_index.end();) { + if (it->first != genesis_hash) { + it = blockman.m_block_index.erase(it); + } else { + ++it; + } + } + chainman.ActiveChainstate().TryAddBlockIndexCandidate(genesis); + assert(blockman.m_block_index.size() == 1); + assert(chainman.ActiveChainstate().setBlockIndexCandidates.size() == 1); + assert(chainman.ActiveChain().Height() == 0); + } +} diff --git a/src/validation.h b/src/validation.h index 9e4fdbe6809..6f9e5e3104f 100644 --- a/src/validation.h +++ b/src/validation.h @@ -768,13 +768,13 @@ public: { return m_mempool ? &m_mempool->cs : nullptr; } + void InvalidBlockFound(CBlockIndex* pindex, const BlockValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + CBlockIndex* FindMostWorkChain() EXCLUSIVE_LOCKS_REQUIRED(cs_main); private: bool ActivateBestChainStep(BlockValidationState& state, CBlockIndex* pindexMostWork, const std::shared_ptr& pblock, bool& fInvalidFound, ConnectTrace& connectTrace) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_mempool->cs); bool ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew, const std::shared_ptr& pblock, ConnectTrace& connectTrace, DisconnectedBlockTransactions& disconnectpool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_mempool->cs); - void InvalidBlockFound(CBlockIndex* pindex, const BlockValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(cs_main); - CBlockIndex* FindMostWorkChain() EXCLUSIVE_LOCKS_REQUIRED(cs_main); bool RollforwardBlock(const CBlockIndex* pindex, CCoinsViewCache& inputs) EXCLUSIVE_LOCKS_REQUIRED(cs_main); @@ -899,7 +899,6 @@ private: //! most-work chain. Chainstate* m_active_chainstate GUARDED_BY(::cs_main) {nullptr}; - CBlockIndex* m_best_invalid GUARDED_BY(::cs_main){nullptr}; /** The last header for which a headerTip notification was issued. */ CBlockIndex* m_last_notified_header GUARDED_BY(GetMutex()){nullptr}; @@ -1061,6 +1060,7 @@ public: /** Best header we've seen so far (used for getheaders queries' starting points). */ CBlockIndex* m_best_header GUARDED_BY(::cs_main){nullptr}; + CBlockIndex* m_best_invalid GUARDED_BY(::cs_main){nullptr}; //! The total number of bytes available for us to use across all in-memory //! coins caches. This will be split somehow across chainstates.