Skip to content

Commit 98d68e0

Browse files
committed
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.
1 parent d5a2ba4 commit 98d68e0

File tree

5 files changed

+214
-3
lines changed

5 files changed

+214
-3
lines changed

src/node/blockstorage.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -1253,4 +1253,11 @@ std::ostream& operator<<(std::ostream& os, const BlockfileCursor& cursor) {
12531253
os << strprintf("BlockfileCursor(file_num=%d, undo_height=%d)", cursor.file_num, cursor.undo_height);
12541254
return os;
12551255
}
1256+
1257+
void BlockManager::CleanupForFuzzing()
1258+
{
1259+
m_dirty_blockindex.clear();
1260+
m_dirty_fileinfo.clear();
1261+
m_blockfile_info.resize(1);
1262+
}
12561263
} // namespace node

src/node/blockstorage.h

+2
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,8 @@ class BlockManager
421421
bool ReadBlockUndo(CBlockUndo& blockundo, const CBlockIndex& index) const;
422422

423423
void CleanupBlockRevFiles() const;
424+
/** Clear internal state (test-only, only for fuzzing) **/
425+
void CleanupForFuzzing();
424426
};
425427

426428
// Calls ActivateBestChain() even if no blocks are imported.

src/test/fuzz/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add_executable(fuzz
1919
block.cpp
2020
block_header.cpp
2121
block_index.cpp
22+
block_index_tree.cpp
2223
blockfilter.cpp
2324
bloom_filter.cpp
2425
buffered_file.cpp

src/test/fuzz/block_index_tree.cpp

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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+
}

src/validation.h

+3-3
Original file line numberDiff line numberDiff line change
@@ -768,13 +768,13 @@ class Chainstate
768768
{
769769
return m_mempool ? &m_mempool->cs : nullptr;
770770
}
771+
void InvalidBlockFound(CBlockIndex* pindex, const BlockValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
772+
CBlockIndex* FindMostWorkChain() EXCLUSIVE_LOCKS_REQUIRED(cs_main);
771773

772774
private:
773775
bool ActivateBestChainStep(BlockValidationState& state, CBlockIndex* pindexMostWork, const std::shared_ptr<const CBlock>& pblock, bool& fInvalidFound, ConnectTrace& connectTrace) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_mempool->cs);
774776
bool ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew, const std::shared_ptr<const CBlock>& pblock, ConnectTrace& connectTrace, DisconnectedBlockTransactions& disconnectpool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_mempool->cs);
775777

776-
void InvalidBlockFound(CBlockIndex* pindex, const BlockValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
777-
CBlockIndex* FindMostWorkChain() EXCLUSIVE_LOCKS_REQUIRED(cs_main);
778778

779779
bool RollforwardBlock(const CBlockIndex* pindex, CCoinsViewCache& inputs) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
780780

@@ -899,7 +899,6 @@ class ChainstateManager
899899
//! most-work chain.
900900
Chainstate* m_active_chainstate GUARDED_BY(::cs_main) {nullptr};
901901

902-
CBlockIndex* m_best_invalid GUARDED_BY(::cs_main){nullptr};
903902

904903
/** The last header for which a headerTip notification was issued. */
905904
CBlockIndex* m_last_notified_header GUARDED_BY(GetMutex()){nullptr};
@@ -1061,6 +1060,7 @@ class ChainstateManager
10611060

10621061
/** Best header we've seen so far (used for getheaders queries' starting points). */
10631062
CBlockIndex* m_best_header GUARDED_BY(::cs_main){nullptr};
1063+
CBlockIndex* m_best_invalid GUARDED_BY(::cs_main){nullptr};
10641064

10651065
//! The total number of bytes available for us to use across all in-memory
10661066
//! coins caches. This will be split somehow across chainstates.

0 commit comments

Comments
 (0)