Coverage Report

Created: 2026-07-08 14:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/tmp/bitcoin/src/index/blockfilterindex.cpp
Line
Count
Source
1
// Copyright (c) 2018-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
#include <index/blockfilterindex.h>
6
7
#include <blockfilter.h>
8
#include <chain.h>
9
#include <common/args.h>
10
#include <dbwrapper.h>
11
#include <flatfile.h>
12
#include <hash.h>
13
#include <index/base.h>
14
#include <index/db_key.h>
15
#include <interfaces/chain.h>
16
#include <interfaces/types.h>
17
#include <serialize.h>
18
#include <streams.h>
19
#include <sync.h>
20
#include <uint256.h>
21
#include <util/check.h>
22
#include <util/fs.h>
23
#include <util/hasher.h>
24
#include <util/log.h>
25
#include <util/syserror.h>
26
27
#include <cerrno>
28
#include <exception>
29
#include <map>
30
#include <optional>
31
#include <stdexcept>
32
#include <string>
33
#include <tuple>
34
#include <utility>
35
#include <vector>
36
37
/* The index database stores three items for each block: the disk location of the encoded filter,
38
 * its dSHA256 hash, and the header. Those belonging to blocks on the active chain are indexed by
39
 * height, and those belonging to blocks that have been reorganized out of the active chain are
40
 * indexed by block hash. This ensures that filter data for any block that becomes part of the
41
 * active chain can always be retrieved, alleviating timing concerns.
42
 *
43
 * The filters themselves are stored in flat files and referenced by the LevelDB entries. This
44
 * minimizes the amount of data written to LevelDB and keeps the database values constant size. The
45
 * disk location of the next block filter to be written (represented as a FlatFilePos) is stored
46
 * under the DB_FILTER_POS key.
47
 *
48
 * The logic for keys is shared with other indexes, see index/db_key.h.
49
 */
50
constexpr uint8_t DB_FILTER_POS{'P'};
51
52
constexpr unsigned int MAX_FLTR_FILE_SIZE{16_MiB};
53
/** The pre-allocation chunk size for fltr?????.dat files */
54
constexpr unsigned int FLTR_FILE_CHUNK_SIZE{1_MiB};
55
/** Maximum size of the cfheaders cache
56
 *  We have a limit to prevent a bug in filling this cache
57
 *  potentially turning into an OOM. At 2000 entries, this cache
58
 *  is big enough for a 2,000,000 length block chain, which
59
 *  we should be enough until ~2047. */
60
constexpr size_t CF_HEADERS_CACHE_MAX_SZ{2000};
61
62
namespace {
63
64
std::string BlockFilterThreadName(BlockFilterType filter_type)
65
44
{
66
44
    switch (filter_type) {
67
44
    case BlockFilterType::BASIC: return "blkfltbscidx";
68
0
    case BlockFilterType::INVALID: return "";
69
44
    } // no default case, so the compiler can warn about missing cases
70
44
    assert(false);
71
0
}
72
73
struct DBVal {
74
    uint256 hash;
75
    uint256 header;
76
    FlatFilePos pos;
77
78
11.7k
    SERIALIZE_METHODS(DBVal, obj) { READWRITE(obj.hash, obj.header, obj.pos); }
blockfilterindex.cpp:void (anonymous namespace)::DBVal::SerializationOps<DataStream, (anonymous namespace)::DBVal, ActionUnserialize>((anonymous namespace)::DBVal&, DataStream&, ActionUnserialize)
Line
Count
Source
78
2.97k
    SERIALIZE_METHODS(DBVal, obj) { READWRITE(obj.hash, obj.header, obj.pos); }
blockfilterindex.cpp:void (anonymous namespace)::DBVal::SerializationOps<SpanReader, (anonymous namespace)::DBVal, ActionUnserialize>((anonymous namespace)::DBVal&, SpanReader&, ActionUnserialize)
Line
Count
Source
78
1.04k
    SERIALIZE_METHODS(DBVal, obj) { READWRITE(obj.hash, obj.header, obj.pos); }
blockfilterindex.cpp:void (anonymous namespace)::DBVal::SerializationOps<DataStream, (anonymous namespace)::DBVal const, ActionSerialize>((anonymous namespace)::DBVal const&, DataStream&, ActionSerialize)
Line
Count
Source
78
7.70k
    SERIALIZE_METHODS(DBVal, obj) { READWRITE(obj.hash, obj.header, obj.pos); }
79
};
80
81
}; // namespace
82
83
static std::map<BlockFilterType, BlockFilterIndex> g_filter_indexes;
84
85
BlockFilterIndex::BlockFilterIndex(std::unique_ptr<interfaces::Chain> chain, BlockFilterType filter_type,
86
                                   size_t n_cache_size, bool f_memory, bool f_wipe)
87
44
    : BaseIndex(std::move(chain), BlockFilterTypeName(filter_type) + " block filter index", BlockFilterThreadName(filter_type))
88
44
    , m_filter_type(filter_type)
89
44
{
90
44
    const std::string& filter_name = BlockFilterTypeName(filter_type);
91
44
    if (filter_name.empty()) throw std::invalid_argument("unknown filter_type");
92
93
44
    fs::path path = gArgs.GetDataDirNet() / "indexes" / "blockfilter" / fs::u8path(filter_name);
94
44
    fs::create_directories(path);
95
96
44
    m_db = std::make_unique<BaseIndex::DB>(path / "db", n_cache_size, f_memory, f_wipe);
97
44
    m_filter_fileseq = std::make_unique<FlatFileSeq>(std::move(path), "fltr", FLTR_FILE_CHUNK_SIZE);
98
44
}
99
100
interfaces::Chain::NotifyOptions BlockFilterIndex::CustomOptions()
101
7.83k
{
102
7.83k
    interfaces::Chain::NotifyOptions options;
103
7.83k
    options.connect_undo_data = true;
104
7.83k
    return options;
105
7.83k
}
106
107
bool BlockFilterIndex::CustomInit(const std::optional<interfaces::BlockRef>& block)
108
43
{
109
43
    if (!m_db->Read(DB_FILTER_POS, m_next_filter_pos)) {
110
        // Check that the cause of the read failure is that the key does not exist. Any other errors
111
        // indicate database corruption or a disk failure, and starting the index would cause
112
        // further corruption.
113
18
        if (m_db->Exists(DB_FILTER_POS)) {
114
0
            LogError("Cannot read current %s state; index may be corrupted",
115
0
                      GetName());
116
0
            return false;
117
0
        }
118
119
        // If the DB_FILTER_POS is not set, then initialize to the first location.
120
18
        m_next_filter_pos.nFile = 0;
121
18
        m_next_filter_pos.nPos = 0;
122
18
    }
123
124
43
    if (block) {
125
25
        auto op_last_header = ReadFilterHeader(block->height, block->hash);
126
25
        if (!op_last_header) {
127
0
            LogError("Cannot read last block filter header; index may be corrupted");
128
0
            return false;
129
0
        }
130
25
        m_last_header = *op_last_header;
131
25
    }
132
133
43
    return true;
134
43
}
135
136
bool BlockFilterIndex::CustomCommit(CDBBatch& batch)
137
73
{
138
73
    const FlatFilePos& pos = m_next_filter_pos;
139
140
    // Flush current filter file to disk.
141
73
    AutoFile file{m_filter_fileseq->Open(pos)};
142
73
    if (file.IsNull()) {
143
0
        LogError("Failed to open filter file %d", pos.nFile);
144
0
        return false;
145
0
    }
146
73
    if (!file.Commit()) {
147
0
        LogError("Failed to commit filter file %d", pos.nFile);
148
0
        (void)file.fclose();
149
0
        return false;
150
0
    }
151
73
    if (file.fclose() != 0) {
152
0
        LogError("Failed to close filter file %d after commit: %s", pos.nFile, SysErrorString(errno));
153
0
        return false;
154
0
    }
155
156
73
    batch.Write(DB_FILTER_POS, pos);
157
73
    return true;
158
73
}
159
160
bool BlockFilterIndex::ReadFilterFromDisk(const FlatFilePos& pos, const uint256& hash, BlockFilter& filter) const
161
1.38k
{
162
1.38k
    AutoFile filein{m_filter_fileseq->Open(pos, true)};
163
1.38k
    if (filein.IsNull()) {
164
0
        return false;
165
0
    }
166
167
    // Check that the hash of the encoded_filter matches the one stored in the db.
168
1.38k
    uint256 block_hash;
169
1.38k
    std::vector<uint8_t> encoded_filter;
170
1.38k
    try {
171
1.38k
        filein >> block_hash >> encoded_filter;
172
1.38k
        if (Hash(encoded_filter) != hash) {
173
0
            LogError("Checksum mismatch in filter decode.");
174
0
            return false;
175
0
        }
176
1.38k
        filter = BlockFilter(GetFilterType(), block_hash, std::move(encoded_filter), /*skip_decode_check=*/true);
177
1.38k
    }
178
1.38k
    catch (const std::exception& e) {
179
0
        LogError("Failed to deserialize block filter from disk: %s", e.what());
180
0
        return false;
181
0
    }
182
183
1.38k
    return true;
184
1.38k
}
185
186
size_t BlockFilterIndex::WriteFilterToDisk(FlatFilePos& pos, const BlockFilter& filter)
187
7.59k
{
188
7.59k
    assert(filter.GetFilterType() == GetFilterType());
189
190
7.59k
    uint64_t data_size{
191
7.59k
        GetSerializeSize(filter.GetBlockHash()) +
192
7.59k
        GetSerializeSize(filter.GetEncodedFilter())};
193
194
    // If writing the filter would overflow the file, flush and move to the next one.
195
7.59k
    if (pos.nPos + data_size > MAX_FLTR_FILE_SIZE) {
196
0
        AutoFile last_file{m_filter_fileseq->Open(pos)};
197
0
        if (last_file.IsNull()) {
198
0
            LogError("Failed to open filter file %d", pos.nFile);
199
0
            return 0;
200
0
        }
201
0
        if (!last_file.Truncate(pos.nPos)) {
202
0
            LogError("Failed to truncate filter file %d", pos.nFile);
203
0
            return 0;
204
0
        }
205
0
        if (!last_file.Commit()) {
206
0
            LogError("Failed to commit filter file %d", pos.nFile);
207
0
            (void)last_file.fclose();
208
0
            return 0;
209
0
        }
210
0
        if (last_file.fclose() != 0) {
211
0
            LogError("Failed to close filter file %d after commit: %s", pos.nFile, SysErrorString(errno));
212
0
            return 0;
213
0
        }
214
215
0
        pos.nFile++;
216
0
        pos.nPos = 0;
217
0
    }
218
219
    // Pre-allocate sufficient space for filter data.
220
7.59k
    bool out_of_space;
221
7.59k
    m_filter_fileseq->Allocate(pos, data_size, out_of_space);
222
7.59k
    if (out_of_space) {
223
0
        LogError("out of disk space");
224
0
        return 0;
225
0
    }
226
227
7.59k
    AutoFile fileout{m_filter_fileseq->Open(pos)};
228
7.59k
    if (fileout.IsNull()) {
229
0
        LogError("Failed to open filter file %d", pos.nFile);
230
0
        return 0;
231
0
    }
232
233
7.59k
    fileout << filter.GetBlockHash() << filter.GetEncodedFilter();
234
235
7.59k
    if (fileout.fclose() != 0) {
236
0
        LogError("Failed to close filter file %d: %s", pos.nFile, SysErrorString(errno));
237
0
        return 0;
238
0
    }
239
240
7.59k
    return data_size;
241
7.59k
}
242
243
std::optional<uint256> BlockFilterIndex::ReadFilterHeader(int height, const uint256& expected_block_hash)
244
136
{
245
136
    std::pair<uint256, DBVal> read_out;
246
136
    if (!m_db->Read(index_util::DBHeightKey(height), read_out)) {
247
0
        return std::nullopt;
248
0
    }
249
250
136
    if (read_out.first != expected_block_hash) {
251
0
        LogError("previous block header belongs to unexpected block %s; expected %s",
252
0
                 read_out.first.ToString(), expected_block_hash.ToString());
253
0
        return std::nullopt;
254
0
    }
255
256
136
    return read_out.second.header;
257
136
}
258
259
bool BlockFilterIndex::CustomAppend(const interfaces::BlockInfo& block)
260
7.59k
{
261
7.59k
    BlockFilter filter(m_filter_type, *Assert(block.data), *Assert(block.undo_data));
262
7.59k
    const uint256& header = filter.ComputeHeader(m_last_header);
263
7.59k
    bool res = Write(filter, block.height, header);
264
7.59k
    if (res) m_last_header = header; // update last header
265
7.59k
    return res;
266
7.59k
}
267
268
bool BlockFilterIndex::Write(const BlockFilter& filter, uint32_t block_height, const uint256& filter_header)
269
7.59k
{
270
7.59k
    size_t bytes_written = WriteFilterToDisk(m_next_filter_pos, filter);
271
7.59k
    if (bytes_written == 0) return false;
272
273
7.59k
    std::pair<uint256, DBVal> value;
274
7.59k
    value.first = filter.GetBlockHash();
275
7.59k
    value.second.hash = filter.GetHash();
276
7.59k
    value.second.header = filter_header;
277
7.59k
    value.second.pos = m_next_filter_pos;
278
279
7.59k
    m_db->Write(index_util::DBHeightKey(block_height), value);
280
281
7.59k
    m_next_filter_pos.nPos += bytes_written;
282
7.59k
    return true;
283
7.59k
}
284
285
bool BlockFilterIndex::CustomRemove(const interfaces::BlockInfo& block)
286
111
{
287
111
    CDBBatch batch(*m_db);
288
111
    std::unique_ptr<CDBIterator> db_it(m_db->NewIterator());
289
290
    // During a reorg, we need to copy block filter that is getting disconnected from the
291
    // height index to the hash index so we can still find it when the height index entry
292
    // is overwritten.
293
111
    if (!index_util::CopyHeightIndexToHashIndex<DBVal>(*db_it, batch, m_name, block.height)) {
294
0
        return false;
295
0
    }
296
297
    // The latest filter position gets written in Commit by the call to the BaseIndex::Rewind.
298
    // But since this creates new references to the filter, the position should get updated here
299
    // atomically as well in case Commit fails.
300
111
    batch.Write(DB_FILTER_POS, m_next_filter_pos);
301
111
    m_db->WriteBatch(batch);
302
303
    // Update cached header to the previous block hash
304
111
    m_last_header = *Assert(ReadFilterHeader(block.height - 1, *Assert(block.prev_hash)));
305
111
    return true;
306
111
}
307
308
static bool LookupRange(CDBWrapper& db, const std::string& index_name, int start_height,
309
                        const CBlockIndex* stop_index, std::vector<DBVal>& results)
310
448
{
311
448
    if (start_height < 0) {
312
0
        LogError("start height (%d) is negative", start_height);
313
0
        return false;
314
0
    }
315
448
    if (start_height > stop_index->nHeight) {
316
0
        LogError("start height (%d) is greater than stop height (%d)",
317
0
                 start_height, stop_index->nHeight);
318
0
        return false;
319
0
    }
320
321
448
    size_t results_size = static_cast<size_t>(stop_index->nHeight - start_height + 1);
322
448
    std::vector<std::pair<uint256, DBVal>> values(results_size);
323
324
448
    index_util::DBHeightKey key(start_height);
325
448
    std::unique_ptr<CDBIterator> db_it(db.NewIterator());
326
448
    db_it->Seek(index_util::DBHeightKey(start_height));
327
3.31k
    for (int height = start_height; height <= stop_index->nHeight; ++height) {
328
3.06k
        if (!db_it->Valid() || !db_it->GetKey(key) || key.height != height) {
329
202
            return false;
330
202
        }
331
332
2.86k
        size_t i = static_cast<size_t>(height - start_height);
333
2.86k
        if (!db_it->GetValue(values[i])) {
334
0
            LogError("unable to read value in %s at key (%c, %d)",
335
0
                     index_name, index_util::DB_BLOCK_HEIGHT, height);
336
0
            return false;
337
0
        }
338
339
2.86k
        db_it->Next();
340
2.86k
    }
341
342
246
    results.resize(results_size);
343
344
    // Iterate backwards through block indexes collecting results in order to access the block hash
345
    // of each entry in case we need to look it up in the hash index.
346
246
    for (const CBlockIndex* block_index = stop_index;
347
3.11k
         block_index && block_index->nHeight >= start_height;
348
2.86k
         block_index = block_index->pprev) {
349
2.86k
        uint256 block_hash = block_index->GetBlockHash();
350
351
2.86k
        size_t i = static_cast<size_t>(block_index->nHeight - start_height);
352
2.86k
        if (block_hash == values[i].first) {
353
2.85k
            results[i] = std::move(values[i].second);
354
2.85k
            continue;
355
2.85k
        }
356
357
12
        if (!db.Read(index_util::DBHashKey(block_hash), results[i])) {
358
0
            LogError("unable to read value in %s at key (%c, %s)",
359
0
                     index_name, index_util::DB_BLOCK_HASH, block_hash.ToString());
360
0
            return false;
361
0
        }
362
12
    }
363
364
246
    return true;
365
246
}
366
367
bool BlockFilterIndex::LookupFilter(const CBlockIndex* block_index, BlockFilter& filter_out) const
368
843
{
369
843
    DBVal entry;
370
843
    if (!index_util::LookUpOne(*m_db, {block_index->GetBlockHash(), block_index->nHeight}, entry)) {
371
101
        return false;
372
101
    }
373
374
742
    return ReadFilterFromDisk(entry.pos, entry.hash, filter_out);
375
843
}
376
377
bool BlockFilterIndex::LookupFilterHeader(const CBlockIndex* block_index, uint256& header_out)
378
240
{
379
240
    LOCK(m_cs_headers_cache);
380
381
240
    bool is_checkpoint{block_index->nHeight % CFCHECKPT_INTERVAL == 0};
382
383
240
    if (is_checkpoint) {
384
        // Try to find the block in the headers cache if this is a checkpoint height.
385
13
        auto header = m_headers_cache.find(block_index->GetBlockHash());
386
13
        if (header != m_headers_cache.end()) {
387
6
            header_out = header->second;
388
6
            return true;
389
6
        }
390
13
    }
391
392
234
    DBVal entry;
393
234
    if (!index_util::LookUpOne(*m_db, {block_index->GetBlockHash(), block_index->nHeight}, entry)) {
394
101
        return false;
395
101
    }
396
397
133
    if (is_checkpoint &&
398
133
        m_headers_cache.size() < CF_HEADERS_CACHE_MAX_SZ) {
399
        // Add to the headers cache if this is a checkpoint height.
400
6
        m_headers_cache.emplace(block_index->GetBlockHash(), entry.header);
401
6
    }
402
403
133
    header_out = entry.header;
404
133
    return true;
405
234
}
406
407
bool BlockFilterIndex::LookupFilterRange(int start_height, const CBlockIndex* stop_index,
408
                                         std::vector<BlockFilter>& filters_out) const
409
230
{
410
230
    std::vector<DBVal> entries;
411
230
    if (!LookupRange(*m_db, m_name, start_height, stop_index, entries)) {
412
101
        return false;
413
101
    }
414
415
129
    filters_out.resize(entries.size());
416
129
    auto filter_pos_it = filters_out.begin();
417
647
    for (const auto& entry : entries) {
418
647
        if (!ReadFilterFromDisk(entry.pos, entry.hash, *filter_pos_it)) {
419
0
            return false;
420
0
        }
421
647
        ++filter_pos_it;
422
647
    }
423
424
129
    return true;
425
129
}
426
427
bool BlockFilterIndex::LookupFilterHashRange(int start_height, const CBlockIndex* stop_index,
428
                                             std::vector<uint256>& hashes_out) const
429
430
218
{
431
218
    std::vector<DBVal> entries;
432
218
    if (!LookupRange(*m_db, m_name, start_height, stop_index, entries)) {
433
101
        return false;
434
101
    }
435
436
117
    hashes_out.clear();
437
117
    hashes_out.reserve(entries.size());
438
2.21k
    for (const auto& entry : entries) {
439
2.21k
        hashes_out.push_back(entry.hash);
440
2.21k
    }
441
117
    return true;
442
218
}
443
444
BlockFilterIndex* GetBlockFilterIndex(BlockFilterType filter_type)
445
1.40k
{
446
1.40k
    auto it = g_filter_indexes.find(filter_type);
447
1.40k
    return it != g_filter_indexes.end() ? &it->second : nullptr;
448
1.40k
}
449
450
void ForEachBlockFilterIndex(std::function<void (BlockFilterIndex&)> fn)
451
58
{
452
58
    for (auto& entry : g_filter_indexes) fn(entry.second);
453
58
}
454
455
bool InitBlockFilterIndex(std::function<std::unique_ptr<interfaces::Chain>()> make_chain, BlockFilterType filter_type,
456
                          size_t n_cache_size, bool f_memory, bool f_wipe)
457
43
{
458
43
    auto result = g_filter_indexes.emplace(std::piecewise_construct,
459
43
                                           std::forward_as_tuple(filter_type),
460
43
                                           std::forward_as_tuple(make_chain(), filter_type,
461
43
                                                                 n_cache_size, f_memory, f_wipe));
462
43
    return result.second;
463
43
}
464
465
bool DestroyBlockFilterIndex(BlockFilterType filter_type)
466
2
{
467
2
    return g_filter_indexes.erase(filter_type);
468
2
}
469
470
void DestroyAllBlockFilterIndexes()
471
1.16k
{
472
1.16k
    g_filter_indexes.clear();
473
1.16k
}