Coverage Report

Created: 2026-10-07 20:26

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