Skip to content

Commit 0b96a19

Browse files
committed
Merge bitcoin/bitcoin#28955: index: block filters sync, reduce disk read operations by caching last header
99afb9d refactor: init, simplify index shutdown code (furszy) 0faafb5 index: decrease ThreadSync cs_main contention (furszy) f1469eb index: cache last block filter header (furszy) a6756ec index: blockfilter, decouple header lookup into its own function (furszy) 331f044 index: blockfilter, decouple Write into its own function (furszy) bcbd7eb bench: basic block filter index initial sync (furszy) Pull request description: Work decoupled from #26966 per request. The aim is to remove an unnecessary disk read operation that currently takes place with every new arriving block (or scanned block during background sync). Instead of reading the last filter header from disk merely to access its hash for constructing the next filter, this work caches it, occupying just 32 more bytes in memory. Also, reduces `cs_main` lock contention during the index initial sync process. And, simplifies the indexes initialization and shutdown procedure. Testing Note: To compare the changes, added a pretty basic benchmark in the second commit. Alternatively, could also test the changes by timing the block filter sync from scratch on any network; start the node with `-blockfilterindex` and monitor the logs until the syncing process finish. Local Benchmark Results: *Master (c252a0f): | ns/op | op/s | err% | total | benchmark |--------------------:|--------------------:|--------:|----------:|:---------- | 132,042,516.60 | 7.57 | 0.3% | 7.79 | `BlockFilterIndexSync` *PR (43a212cfdac6c64e82b601c664443d022f191520): | ns/op | op/s | err% | total | benchmark |--------------------:|--------------------:|--------:|----------:|:---------- | 126,915,841.60 | 7.88 | 0.6% | 7.51 | `BlockFilterIndexSync` ACKs for top commit: Sjors: re-ACK 99afb9d achow101: ACK 99afb9d TheCharlatan: Re-ACK 99afb9d andrewtoth: ACK 99afb9d Tree-SHA512: 927daadd68f4ee1ca781a89519539b895f5185a76ebaf525fbc246ea8dcf40d44a82def00ac34b188640802844b312270067f1b33e65a2479e06be9169c616de
2 parents 5b9831a + 99afb9d commit 0b96a19

File tree

7 files changed

+119
-59
lines changed

7 files changed

+119
-59
lines changed

src/Makefile.bench.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ bench_bench_bitcoin_SOURCES = \
3434
bench/examples.cpp \
3535
bench/gcs_filter.cpp \
3636
bench/hashpadding.cpp \
37+
bench/index_blockfilter.cpp \
3738
bench/load_external.cpp \
3839
bench/lockedpool.cpp \
3940
bench/logging.cpp \

src/bench/index_blockfilter.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2023-present The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or https://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <bench/bench.h>
6+
7+
#include <addresstype.h>
8+
#include <index/blockfilterindex.h>
9+
#include <node/chainstate.h>
10+
#include <node/context.h>
11+
#include <test/util/setup_common.h>
12+
#include <util/strencodings.h>
13+
14+
// Very simple block filter index sync benchmark, only using coinbase outputs.
15+
static void BlockFilterIndexSync(benchmark::Bench& bench)
16+
{
17+
const auto test_setup = MakeNoLogFileContext<TestChain100Setup>();
18+
19+
// Create more blocks
20+
int CHAIN_SIZE = 600;
21+
CPubKey pubkey{ParseHex("02ed26169896db86ced4cbb7b3ecef9859b5952825adbeab998fb5b307e54949c9")};
22+
CScript script = GetScriptForDestination(WitnessV0KeyHash(pubkey));
23+
std::vector<CMutableTransaction> noTxns;
24+
for (int i = 0; i < CHAIN_SIZE - 100; i++) {
25+
test_setup->CreateAndProcessBlock(noTxns, script);
26+
SetMockTime(GetTime() + 1);
27+
}
28+
assert(WITH_LOCK(::cs_main, return test_setup->m_node.chainman->ActiveHeight() == CHAIN_SIZE));
29+
30+
bench.minEpochIterations(5).run([&] {
31+
BlockFilterIndex filter_index(interfaces::MakeChain(test_setup->m_node), BlockFilterType::BASIC,
32+
/*n_cache_size=*/0, /*f_memory=*/false, /*f_wipe=*/true);
33+
assert(filter_index.Init());
34+
assert(!filter_index.BlockUntilSyncedToCurrentChain());
35+
filter_index.Sync();
36+
37+
IndexSummary summary = filter_index.GetSummary();
38+
assert(summary.synced);
39+
assert(summary.best_block_hash == WITH_LOCK(::cs_main, return test_setup->m_node.chainman->ActiveTip()->GetBlockHash()));
40+
});
41+
}
42+
43+
BENCHMARK(BlockFilterIndexSync, benchmark::PriorityLevel::HIGH);

src/index/base.cpp

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ static const CBlockIndex* NextSyncBlock(const CBlockIndex* pindex_prev, CChain&
141141
return chain.Next(chain.FindFork(pindex_prev));
142142
}
143143

144-
void BaseIndex::ThreadSync()
144+
void BaseIndex::Sync()
145145
{
146146
const CBlockIndex* pindex = m_best_block_index.load();
147147
if (!m_synced) {
@@ -159,23 +159,20 @@ void BaseIndex::ThreadSync()
159159
return;
160160
}
161161

162-
{
163-
LOCK(cs_main);
164-
const CBlockIndex* pindex_next = NextSyncBlock(pindex, m_chainstate->m_chain);
165-
if (!pindex_next) {
166-
SetBestBlockIndex(pindex);
167-
m_synced = true;
168-
// No need to handle errors in Commit. See rationale above.
169-
Commit();
170-
break;
171-
}
172-
if (pindex_next->pprev != pindex && !Rewind(pindex, pindex_next->pprev)) {
173-
FatalErrorf("%s: Failed to rewind index %s to a previous chain tip",
174-
__func__, GetName());
175-
return;
176-
}
177-
pindex = pindex_next;
162+
const CBlockIndex* pindex_next = WITH_LOCK(cs_main, return NextSyncBlock(pindex, m_chainstate->m_chain));
163+
if (!pindex_next) {
164+
SetBestBlockIndex(pindex);
165+
m_synced = true;
166+
// No need to handle errors in Commit. See rationale above.
167+
Commit();
168+
break;
178169
}
170+
if (pindex_next->pprev != pindex && !Rewind(pindex, pindex_next->pprev)) {
171+
FatalErrorf("%s: Failed to rewind index %s to a previous chain tip", __func__, GetName());
172+
return;
173+
}
174+
pindex = pindex_next;
175+
179176

180177
auto current_time{std::chrono::steady_clock::now()};
181178
if (last_log_time + SYNC_LOG_INTERVAL < current_time) {
@@ -394,7 +391,7 @@ bool BaseIndex::StartBackgroundSync()
394391
{
395392
if (!m_init) throw std::logic_error("Error: Cannot start a non-initialized index");
396393

397-
m_thread_sync = std::thread(&util::TraceThread, GetName(), [this] { ThreadSync(); });
394+
m_thread_sync = std::thread(&util::TraceThread, GetName(), [this] { Sync(); });
398395
return true;
399396
}
400397

src/index/base.h

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,6 @@ class BaseIndex : public CValidationInterface
7878
std::thread m_thread_sync;
7979
CThreadInterrupt m_interrupt;
8080

81-
/// Sync the index with the block index starting from the current best block.
82-
/// Intended to be run in its own thread, m_thread_sync, and can be
83-
/// interrupted with m_interrupt. Once the index gets in sync, the m_synced
84-
/// flag is set and the BlockConnected ValidationInterface callback takes
85-
/// over and the sync thread exits.
86-
void ThreadSync();
87-
8881
/// Write the current index state (eg. chain block locator and subclass-specific items) to disk.
8982
///
9083
/// Recommendations for error handling:
@@ -152,9 +145,16 @@ class BaseIndex : public CValidationInterface
152145
/// validation interface so that it stays in sync with blockchain updates.
153146
[[nodiscard]] bool Init();
154147

155-
/// Starts the initial sync process.
148+
/// Starts the initial sync process on a background thread.
156149
[[nodiscard]] bool StartBackgroundSync();
157150

151+
/// Sync the index with the block index starting from the current best block.
152+
/// Intended to be run in its own thread, m_thread_sync, and can be
153+
/// interrupted with m_interrupt. Once the index gets in sync, the m_synced
154+
/// flag is set and the BlockConnected ValidationInterface callback takes
155+
/// over and the sync thread exits.
156+
void Sync();
157+
158158
/// Stops the instance from staying in sync with blockchain updates.
159159
void Stop();
160160

src/index/blockfilterindex.cpp

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ bool BlockFilterIndex::CustomInit(const std::optional<interfaces::BlockKey>& blo
128128
m_next_filter_pos.nFile = 0;
129129
m_next_filter_pos.nPos = 0;
130130
}
131+
132+
if (block) {
133+
auto op_last_header = ReadFilterHeader(block->height, block->hash);
134+
if (!op_last_header) {
135+
LogError("Cannot read last block filter header; index may be corrupted\n");
136+
return false;
137+
}
138+
m_last_header = *op_last_header;
139+
}
140+
131141
return true;
132142
}
133143

@@ -222,10 +232,25 @@ size_t BlockFilterIndex::WriteFilterToDisk(FlatFilePos& pos, const BlockFilter&
222232
return data_size;
223233
}
224234

235+
std::optional<uint256> BlockFilterIndex::ReadFilterHeader(int height, const uint256& expected_block_hash)
236+
{
237+
std::pair<uint256, DBVal> read_out;
238+
if (!m_db->Read(DBHeightKey(height), read_out)) {
239+
return std::nullopt;
240+
}
241+
242+
if (read_out.first != expected_block_hash) {
243+
LogError("%s: previous block header belongs to unexpected block %s; expected %s\n",
244+
__func__, read_out.first.ToString(), expected_block_hash.ToString());
245+
return std::nullopt;
246+
}
247+
248+
return read_out.second.header;
249+
}
250+
225251
bool BlockFilterIndex::CustomAppend(const interfaces::BlockInfo& block)
226252
{
227253
CBlockUndo block_undo;
228-
uint256 prev_header;
229254

230255
if (block.height > 0) {
231256
// pindex variable gives indexing code access to node internals. It
@@ -234,34 +259,28 @@ bool BlockFilterIndex::CustomAppend(const interfaces::BlockInfo& block)
234259
if (!m_chainstate->m_blockman.UndoReadFromDisk(block_undo, *pindex)) {
235260
return false;
236261
}
237-
238-
std::pair<uint256, DBVal> read_out;
239-
if (!m_db->Read(DBHeightKey(block.height - 1), read_out)) {
240-
return false;
241-
}
242-
243-
uint256 expected_block_hash = *Assert(block.prev_hash);
244-
if (read_out.first != expected_block_hash) {
245-
LogError("%s: previous block header belongs to unexpected block %s; expected %s\n",
246-
__func__, read_out.first.ToString(), expected_block_hash.ToString());
247-
return false;
248-
}
249-
250-
prev_header = read_out.second.header;
251262
}
252263

253264
BlockFilter filter(m_filter_type, *Assert(block.data), block_undo);
254265

266+
const uint256& header = filter.ComputeHeader(m_last_header);
267+
bool res = Write(filter, block.height, header);
268+
if (res) m_last_header = header; // update last header
269+
return res;
270+
}
271+
272+
bool BlockFilterIndex::Write(const BlockFilter& filter, uint32_t block_height, const uint256& filter_header)
273+
{
255274
size_t bytes_written = WriteFilterToDisk(m_next_filter_pos, filter);
256275
if (bytes_written == 0) return false;
257276

258277
std::pair<uint256, DBVal> value;
259-
value.first = block.hash;
278+
value.first = filter.GetBlockHash();
260279
value.second.hash = filter.GetHash();
261-
value.second.header = filter.ComputeHeader(prev_header);
280+
value.second.header = filter_header;
262281
value.second.pos = m_next_filter_pos;
263282

264-
if (!m_db->Write(DBHeightKey(block.height), value)) {
283+
if (!m_db->Write(DBHeightKey(block_height), value)) {
265284
return false;
266285
}
267286

@@ -315,6 +334,8 @@ bool BlockFilterIndex::CustomRewind(const interfaces::BlockKey& current_tip, con
315334
batch.Write(DB_FILTER_POS, m_next_filter_pos);
316335
if (!m_db->WriteBatch(batch)) return false;
317336

337+
// Update cached header
338+
m_last_header = *Assert(ReadFilterHeader(new_tip.height, new_tip.hash));
318339
return true;
319340
}
320341

src/index/blockfilterindex.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,15 @@ class BlockFilterIndex final : public BaseIndex
4242
/** cache of block hash to filter header, to avoid disk access when responding to getcfcheckpt. */
4343
std::unordered_map<uint256, uint256, FilterHeaderHasher> m_headers_cache GUARDED_BY(m_cs_headers_cache);
4444

45+
// Last computed header to avoid disk reads on every new block.
46+
uint256 m_last_header{};
47+
4548
bool AllowPrune() const override { return true; }
4649

50+
bool Write(const BlockFilter& filter, uint32_t block_height, const uint256& filter_header);
51+
52+
std::optional<uint256> ReadFilterHeader(int height, const uint256& expected_block_hash);
53+
4754
protected:
4855
bool CustomInit(const std::optional<interfaces::BlockKey>& block) override;
4956

src/init.cpp

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,8 @@ void Interrupt(NodeContext& node)
256256
InterruptMapPort();
257257
if (node.connman)
258258
node.connman->Interrupt();
259-
if (g_txindex) {
260-
g_txindex->Interrupt();
261-
}
262-
ForEachBlockFilterIndex([](BlockFilterIndex& index) { index.Interrupt(); });
263-
if (g_coin_stats_index) {
264-
g_coin_stats_index->Interrupt();
259+
for (auto* index : node.indexes) {
260+
index->Interrupt();
265261
}
266262
}
267263

@@ -337,16 +333,11 @@ void Shutdown(NodeContext& node)
337333
if (node.validation_signals) node.validation_signals->FlushBackgroundCallbacks();
338334

339335
// Stop and delete all indexes only after flushing background callbacks.
340-
if (g_txindex) {
341-
g_txindex->Stop();
342-
g_txindex.reset();
343-
}
344-
if (g_coin_stats_index) {
345-
g_coin_stats_index->Stop();
346-
g_coin_stats_index.reset();
347-
}
348-
ForEachBlockFilterIndex([](BlockFilterIndex& index) { index.Stop(); });
336+
for (auto* index : node.indexes) index->Stop();
337+
if (g_txindex) g_txindex.reset();
338+
if (g_coin_stats_index) g_coin_stats_index.reset();
349339
DestroyAllBlockFilterIndexes();
340+
node.indexes.clear(); // all instances are nullptr now
350341

351342
// Any future callbacks will be dropped. This should absolutely be safe - if
352343
// missing a callback results in an unrecoverable situation, unclean shutdown

0 commit comments

Comments
 (0)