Coverage Report

Created: 2026-05-06 07:53

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
struct DBVal {
65
    uint256 hash;
66
    uint256 header;
67
    FlatFilePos pos;
68
69
11.6k
    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
69
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
69
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
69
7.65k
    SERIALIZE_METHODS(DBVal, obj) { READWRITE(obj.hash, obj.header, obj.pos); }
70
};
71
72
}; // namespace
73
74
static std::map<BlockFilterType, BlockFilterIndex> g_filter_indexes;
75
76
BlockFilterIndex::BlockFilterIndex(std::unique_ptr<interfaces::Chain> chain, BlockFilterType filter_type,
77
                                   size_t n_cache_size, bool f_memory, bool f_wipe)
78
43
    : BaseIndex(std::move(chain), BlockFilterTypeName(filter_type) + " block filter index")
79
43
    , m_filter_type(filter_type)
80
43
{
81
43
    const std::string& filter_name = BlockFilterTypeName(filter_type);
82
43
    if (filter_name.empty()) throw std::invalid_argument("unknown filter_type");
83
84
43
    fs::path path = gArgs.GetDataDirNet() / "indexes" / "blockfilter" / fs::u8path(filter_name);
85
43
    fs::create_directories(path);
86
87
43
    m_db = std::make_unique<BaseIndex::DB>(path / "db", n_cache_size, f_memory, f_wipe);
88
43
    m_filter_fileseq = std::make_unique<FlatFileSeq>(std::move(path), "fltr", FLTR_FILE_CHUNK_SIZE);
89
43
}
90
91
interfaces::Chain::NotifyOptions BlockFilterIndex::CustomOptions()
92
7.78k
{
93
7.78k
    interfaces::Chain::NotifyOptions options;
94
7.78k
    options.connect_undo_data = true;
95
7.78k
    return options;
96
7.78k
}
97
98
bool BlockFilterIndex::CustomInit(const std::optional<interfaces::BlockRef>& block)
99
42
{
100
42
    if (!m_db->Read(DB_FILTER_POS, m_next_filter_pos)) {
101
        // Check that the cause of the read failure is that the key does not exist. Any other errors
102
        // indicate database corruption or a disk failure, and starting the index would cause
103
        // further corruption.
104
17
        if (m_db->Exists(DB_FILTER_POS)) {
105
0
            LogError("Cannot read current %s state; index may be corrupted",
106
0
                      GetName());
107
0
            return false;
108
0
        }
109
110
        // If the DB_FILTER_POS is not set, then initialize to the first location.
111
17
        m_next_filter_pos.nFile = 0;
112
17
        m_next_filter_pos.nPos = 0;
113
17
    }
114
115
42
    if (block) {
116
25
        auto op_last_header = ReadFilterHeader(block->height, block->hash);
117
25
        if (!op_last_header) {
118
0
            LogError("Cannot read last block filter header; index may be corrupted");
119
0
            return false;
120
0
        }
121
25
        m_last_header = *op_last_header;
122
25
    }
123
124
42
    return true;
125
42
}
126
127
bool BlockFilterIndex::CustomCommit(CDBBatch& batch)
128
72
{
129
72
    const FlatFilePos& pos = m_next_filter_pos;
130
131
    // Flush current filter file to disk.
132
72
    AutoFile file{m_filter_fileseq->Open(pos)};
133
72
    if (file.IsNull()) {
134
0
        LogError("Failed to open filter file %d", pos.nFile);
135
0
        return false;
136
0
    }
137
72
    if (!file.Commit()) {
138
0
        LogError("Failed to commit filter file %d", pos.nFile);
139
0
        (void)file.fclose();
140
0
        return false;
141
0
    }
142
72
    if (file.fclose() != 0) {
143
0
        LogError("Failed to close filter file %d after commit: %s", pos.nFile, SysErrorString(errno));
144
0
        return false;
145
0
    }
146
147
72
    batch.Write(DB_FILTER_POS, pos);
148
72
    return true;
149
72
}
150
151
bool BlockFilterIndex::ReadFilterFromDisk(const FlatFilePos& pos, const uint256& hash, BlockFilter& filter) const
152
1.38k
{
153
1.38k
    AutoFile filein{m_filter_fileseq->Open(pos, true)};
154
1.38k
    if (filein.IsNull()) {
155
0
        return false;
156
0
    }
157
158
    // Check that the hash of the encoded_filter matches the one stored in the db.
159
1.38k
    uint256 block_hash;
160
1.38k
    std::vector<uint8_t> encoded_filter;
161
1.38k
    try {
162
1.38k
        filein >> block_hash >> encoded_filter;
163
1.38k
        if (Hash(encoded_filter) != hash) {
164
0
            LogError("Checksum mismatch in filter decode.");
165
0
            return false;
166
0
        }
167
1.38k
        filter = BlockFilter(GetFilterType(), block_hash, std::move(encoded_filter), /*skip_decode_check=*/true);
168
1.38k
    }
169
1.38k
    catch (const std::exception& e) {
170
0
        LogError("Failed to deserialize block filter from disk: %s", e.what());
171
0
        return false;
172
0
    }
173
174
1.38k
    return true;
175
1.38k
}
176
177
size_t BlockFilterIndex::WriteFilterToDisk(FlatFilePos& pos, const BlockFilter& filter)
178
7.54k
{
179
7.54k
    assert(filter.GetFilterType() == GetFilterType());
180
181
7.54k
    uint64_t data_size{
182
7.54k
        GetSerializeSize(filter.GetBlockHash()) +
183
7.54k
        GetSerializeSize(filter.GetEncodedFilter())};
184
185
    // If writing the filter would overflow the file, flush and move to the next one.
186
7.54k
    if (pos.nPos + data_size > MAX_FLTR_FILE_SIZE) {
187
0
        AutoFile last_file{m_filter_fileseq->Open(pos)};
188
0
        if (last_file.IsNull()) {
189
0
            LogError("Failed to open filter file %d", pos.nFile);
190
0
            return 0;
191
0
        }
192
0
        if (!last_file.Truncate(pos.nPos)) {
193
0
            LogError("Failed to truncate filter file %d", pos.nFile);
194
0
            return 0;
195
0
        }
196
0
        if (!last_file.Commit()) {
197
0
            LogError("Failed to commit filter file %d", pos.nFile);
198
0
            (void)last_file.fclose();
199
0
            return 0;
200
0
        }
201
0
        if (last_file.fclose() != 0) {
202
0
            LogError("Failed to close filter file %d after commit: %s", pos.nFile, SysErrorString(errno));
203
0
            return 0;
204
0
        }
205
206
0
        pos.nFile++;
207
0
        pos.nPos = 0;
208
0
    }
209
210
    // Pre-allocate sufficient space for filter data.
211
7.54k
    bool out_of_space;
212
7.54k
    m_filter_fileseq->Allocate(pos, data_size, out_of_space);
213
7.54k
    if (out_of_space) {
214
0
        LogError("out of disk space");
215
0
        return 0;
216
0
    }
217
218
7.54k
    AutoFile fileout{m_filter_fileseq->Open(pos)};
219
7.54k
    if (fileout.IsNull()) {
220
0
        LogError("Failed to open filter file %d", pos.nFile);
221
0
        return 0;
222
0
    }
223
224
7.54k
    fileout << filter.GetBlockHash() << filter.GetEncodedFilter();
225
226
7.54k
    if (fileout.fclose() != 0) {
227
0
        LogError("Failed to close filter file %d: %s", pos.nFile, SysErrorString(errno));
228
0
        return 0;
229
0
    }
230
231
7.54k
    return data_size;
232
7.54k
}
233
234
std::optional<uint256> BlockFilterIndex::ReadFilterHeader(int height, const uint256& expected_block_hash)
235
136
{
236
136
    std::pair<uint256, DBVal> read_out;
237
136
    if (!m_db->Read(index_util::DBHeightKey(height), read_out)) {
238
0
        return std::nullopt;
239
0
    }
240
241
136
    if (read_out.first != expected_block_hash) {
242
0
        LogError("previous block header belongs to unexpected block %s; expected %s",
243
0
                 read_out.first.ToString(), expected_block_hash.ToString());
244
0
        return std::nullopt;
245
0
    }
246
247
136
    return read_out.second.header;
248
136
}
249
250
bool BlockFilterIndex::CustomAppend(const interfaces::BlockInfo& block)
251
7.54k
{
252
7.54k
    BlockFilter filter(m_filter_type, *Assert(block.data), *Assert(block.undo_data));
253
7.54k
    const uint256& header = filter.ComputeHeader(m_last_header);
254
7.54k
    bool res = Write(filter, block.height, header);
255
7.54k
    if (res) m_last_header = header; // update last header
256
7.54k
    return res;
257
7.54k
}
258
259
bool BlockFilterIndex::Write(const BlockFilter& filter, uint32_t block_height, const uint256& filter_header)
260
7.54k
{
261
7.54k
    size_t bytes_written = WriteFilterToDisk(m_next_filter_pos, filter);
262
7.54k
    if (bytes_written == 0) return false;
263
264
7.54k
    std::pair<uint256, DBVal> value;
265
7.54k
    value.first = filter.GetBlockHash();
266
7.54k
    value.second.hash = filter.GetHash();
267
7.54k
    value.second.header = filter_header;
268
7.54k
    value.second.pos = m_next_filter_pos;
269
270
7.54k
    m_db->Write(index_util::DBHeightKey(block_height), value);
271
272
7.54k
    m_next_filter_pos.nPos += bytes_written;
273
7.54k
    return true;
274
7.54k
}
275
276
bool BlockFilterIndex::CustomRemove(const interfaces::BlockInfo& block)
277
111
{
278
111
    CDBBatch batch(*m_db);
279
111
    std::unique_ptr<CDBIterator> db_it(m_db->NewIterator());
280
281
    // During a reorg, we need to copy block filter that is getting disconnected from the
282
    // height index to the hash index so we can still find it when the height index entry
283
    // is overwritten.
284
111
    if (!index_util::CopyHeightIndexToHashIndex<DBVal>(*db_it, batch, m_name, block.height)) {
285
0
        return false;
286
0
    }
287
288
    // The latest filter position gets written in Commit by the call to the BaseIndex::Rewind.
289
    // But since this creates new references to the filter, the position should get updated here
290
    // atomically as well in case Commit fails.
291
111
    batch.Write(DB_FILTER_POS, m_next_filter_pos);
292
111
    m_db->WriteBatch(batch);
293
294
    // Update cached header to the previous block hash
295
111
    m_last_header = *Assert(ReadFilterHeader(block.height - 1, *Assert(block.prev_hash)));
296
111
    return true;
297
111
}
298
299
static bool LookupRange(CDBWrapper& db, const std::string& index_name, int start_height,
300
                        const CBlockIndex* stop_index, std::vector<DBVal>& results)
301
448
{
302
448
    if (start_height < 0) {
303
0
        LogError("start height (%d) is negative", start_height);
304
0
        return false;
305
0
    }
306
448
    if (start_height > stop_index->nHeight) {
307
0
        LogError("start height (%d) is greater than stop height (%d)",
308
0
                 start_height, stop_index->nHeight);
309
0
        return false;
310
0
    }
311
312
448
    size_t results_size = static_cast<size_t>(stop_index->nHeight - start_height + 1);
313
448
    std::vector<std::pair<uint256, DBVal>> values(results_size);
314
315
448
    index_util::DBHeightKey key(start_height);
316
448
    std::unique_ptr<CDBIterator> db_it(db.NewIterator());
317
448
    db_it->Seek(index_util::DBHeightKey(start_height));
318
3.31k
    for (int height = start_height; height <= stop_index->nHeight; ++height) {
319
3.06k
        if (!db_it->Valid() || !db_it->GetKey(key) || key.height != height) {
320
202
            return false;
321
202
        }
322
323
2.86k
        size_t i = static_cast<size_t>(height - start_height);
324
2.86k
        if (!db_it->GetValue(values[i])) {
325
0
            LogError("unable to read value in %s at key (%c, %d)",
326
0
                     index_name, index_util::DB_BLOCK_HEIGHT, height);
327
0
            return false;
328
0
        }
329
330
2.86k
        db_it->Next();
331
2.86k
    }
332
333
246
    results.resize(results_size);
334
335
    // Iterate backwards through block indexes collecting results in order to access the block hash
336
    // of each entry in case we need to look it up in the hash index.
337
246
    for (const CBlockIndex* block_index = stop_index;
338
3.11k
         block_index && block_index->nHeight >= start_height;
339
2.86k
         block_index = block_index->pprev) {
340
2.86k
        uint256 block_hash = block_index->GetBlockHash();
341
342
2.86k
        size_t i = static_cast<size_t>(block_index->nHeight - start_height);
343
2.86k
        if (block_hash == values[i].first) {
344
2.85k
            results[i] = std::move(values[i].second);
345
2.85k
            continue;
346
2.85k
        }
347
348
12
        if (!db.Read(index_util::DBHashKey(block_hash), results[i])) {
349
0
            LogError("unable to read value in %s at key (%c, %s)",
350
0
                     index_name, index_util::DB_BLOCK_HASH, block_hash.ToString());
351
0
            return false;
352
0
        }
353
12
    }
354
355
246
    return true;
356
246
}
357
358
bool BlockFilterIndex::LookupFilter(const CBlockIndex* block_index, BlockFilter& filter_out) const
359
843
{
360
843
    DBVal entry;
361
843
    if (!index_util::LookUpOne(*m_db, {block_index->GetBlockHash(), block_index->nHeight}, entry)) {
362
101
        return false;
363
101
    }
364
365
742
    return ReadFilterFromDisk(entry.pos, entry.hash, filter_out);
366
843
}
367
368
bool BlockFilterIndex::LookupFilterHeader(const CBlockIndex* block_index, uint256& header_out)
369
240
{
370
240
    LOCK(m_cs_headers_cache);
371
372
240
    bool is_checkpoint{block_index->nHeight % CFCHECKPT_INTERVAL == 0};
373
374
240
    if (is_checkpoint) {
375
        // Try to find the block in the headers cache if this is a checkpoint height.
376
13
        auto header = m_headers_cache.find(block_index->GetBlockHash());
377
13
        if (header != m_headers_cache.end()) {
378
6
            header_out = header->second;
379
6
            return true;
380
6
        }
381
13
    }
382
383
234
    DBVal entry;
384
234
    if (!index_util::LookUpOne(*m_db, {block_index->GetBlockHash(), block_index->nHeight}, entry)) {
385
101
        return false;
386
101
    }
387
388
133
    if (is_checkpoint &&
389
133
        m_headers_cache.size() < CF_HEADERS_CACHE_MAX_SZ) {
390
        // Add to the headers cache if this is a checkpoint height.
391
6
        m_headers_cache.emplace(block_index->GetBlockHash(), entry.header);
392
6
    }
393
394
133
    header_out = entry.header;
395
133
    return true;
396
234
}
397
398
bool BlockFilterIndex::LookupFilterRange(int start_height, const CBlockIndex* stop_index,
399
                                         std::vector<BlockFilter>& filters_out) const
400
230
{
401
230
    std::vector<DBVal> entries;
402
230
    if (!LookupRange(*m_db, m_name, start_height, stop_index, entries)) {
403
101
        return false;
404
101
    }
405
406
129
    filters_out.resize(entries.size());
407
129
    auto filter_pos_it = filters_out.begin();
408
647
    for (const auto& entry : entries) {
409
647
        if (!ReadFilterFromDisk(entry.pos, entry.hash, *filter_pos_it)) {
410
0
            return false;
411
0
        }
412
647
        ++filter_pos_it;
413
647
    }
414
415
129
    return true;
416
129
}
417
418
bool BlockFilterIndex::LookupFilterHashRange(int start_height, const CBlockIndex* stop_index,
419
                                             std::vector<uint256>& hashes_out) const
420
421
218
{
422
218
    std::vector<DBVal> entries;
423
218
    if (!LookupRange(*m_db, m_name, start_height, stop_index, entries)) {
424
101
        return false;
425
101
    }
426
427
117
    hashes_out.clear();
428
117
    hashes_out.reserve(entries.size());
429
2.21k
    for (const auto& entry : entries) {
430
2.21k
        hashes_out.push_back(entry.hash);
431
2.21k
    }
432
117
    return true;
433
218
}
434
435
BlockFilterIndex* GetBlockFilterIndex(BlockFilterType filter_type)
436
1.38k
{
437
1.38k
    auto it = g_filter_indexes.find(filter_type);
438
1.38k
    return it != g_filter_indexes.end() ? &it->second : nullptr;
439
1.38k
}
440
441
void ForEachBlockFilterIndex(std::function<void (BlockFilterIndex&)> fn)
442
58
{
443
58
    for (auto& entry : g_filter_indexes) fn(entry.second);
444
58
}
445
446
bool InitBlockFilterIndex(std::function<std::unique_ptr<interfaces::Chain>()> make_chain, BlockFilterType filter_type,
447
                          size_t n_cache_size, bool f_memory, bool f_wipe)
448
42
{
449
42
    auto result = g_filter_indexes.emplace(std::piecewise_construct,
450
42
                                           std::forward_as_tuple(filter_type),
451
42
                                           std::forward_as_tuple(make_chain(), filter_type,
452
42
                                                                 n_cache_size, f_memory, f_wipe));
453
42
    return result.second;
454
42
}
455
456
bool DestroyBlockFilterIndex(BlockFilterType filter_type)
457
2
{
458
2
    return g_filter_indexes.erase(filter_type);
459
2
}
460
461
void DestroyAllBlockFilterIndexes()
462
1.13k
{
463
1.13k
    g_filter_indexes.clear();
464
1.13k
}