Repository navigation
Expand file tree
/
Copy pathbaseindex_tests.cpp
More file actions
244 lines (212 loc) · 10.4 KB
/
Copy pathbaseindex_tests.cpp
File metadata and controls
244 lines (212 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// Copyright (c) 2020-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <addresstype.h>
#include <blockfilter.h>
#include <chain.h>
#include <chainparams.h>
#include <coins.h>
#include <common/args.h>
#include <consensus/validation.h>
#include <index/base.h>
#include <index/blockfilterindex.h>
#include <index/coinstatsindex.h>
#include <index/txindex.h>
#include <index/txospenderindex.h>
#include <interfaces/chain.h>
#include <kernel/types.h>
#include <key.h>
#include <node/context.h>
#include <primitives/block.h>
#include <script/script.h>
#include <sync.h>
#include <test/util/mining.h>
#include <test/util/setup_common.h>
#include <test/util/time.h>
#include <test/util/validation.h>
#include <tinyformat.h>
#include <util/byte_units.h>
#include <util/check.h>
#include <util/fs.h>
#include <validation.h>
#include <boost/test/unit_test.hpp>
#include <chrono>
#include <functional>
#include <future>
#include <memory>
#include <string>
#include <thread>
#include <utility>
#include <vector>
using kernel::ChainstateRole;
using IndexFactory = std::function<std::unique_ptr<BaseIndex>(node::NodeContext&)>;
static const std::vector<std::pair<std::string, IndexFactory>> INDEX_FACTORIES{
{"coinstatsindex", [](node::NodeContext& node) -> std::unique_ptr<BaseIndex> {
return std::make_unique<CoinStatsIndex>(interfaces::MakeChain(node), /*n_cache_size=*/1_MiB); }},
{"txindex", [](node::NodeContext& node) -> std::unique_ptr<BaseIndex> {
return std::make_unique<TxIndex>(interfaces::MakeChain(node), /*n_cache_size=*/1_MiB); }},
{"txospenderindex", [](node::NodeContext& node) -> std::unique_ptr<BaseIndex> {
return std::make_unique<TxoSpenderIndex>(interfaces::MakeChain(node), /*n_cache_size=*/1_MiB); }},
{"blockfilterindex", [](node::NodeContext& node) -> std::unique_ptr<BaseIndex> {
return std::make_unique<BlockFilterIndex>(interfaces::MakeChain(node), BlockFilterType::BASIC, /*n_cache_size=*/1_MiB); }},
};
// Tests of generic BaseIndex functionality that is independent of which
// concrete index is being used.
BOOST_AUTO_TEST_SUITE(baseindex_tests)
// Test that the index does not commit ahead of the chainstate's last
// flushed block. If it did, a subsequent unclean shutdown would corrupt
// the index, because during reverting it would require blocks that were
// never flushed to disk.
BOOST_FIXTURE_TEST_CASE(baseindex_no_commit_ahead_of_flush, TestChain100Setup)
{
Chainstate& chainstate = Assert(m_node.chainman)->ActiveChainstate();
for (const auto& [index_name, make_index] : INDEX_FACTORIES) {
BOOST_TEST_INFO_SCOPE(index_name);
const int tip_height{WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Tip()->nHeight)};
auto sync_index = [&](bool do_flush, int expected_sync_height, int expected_commit_height) {
auto index{make_index(m_node)};
BOOST_REQUIRE(index->Init());
index->Sync();
if (do_flush) {
chainstate.ForceFlushStateToDisk();
m_node.chain->context()->validation_signals->SyncWithValidationInterfaceQueue();
}
BOOST_CHECK_EQUAL(index->GetSummary().best_block_height, expected_sync_height);
index->Stop();
// Reload index to see which block data was actually committed.
BOOST_REQUIRE(index->Init());
BOOST_CHECK_EQUAL(index->GetSummary().best_block_height, expected_commit_height);
// Drain in-flight validation callbacks before destroying the index.
m_node.chain->context()->validation_signals->SyncWithValidationInterfaceQueue();
// shutdown sequence (c.f. Shutdown() in init.cpp)
index->Stop();
};
// Part 1: Sync, then "crash" (stop without flushing). Models a node that
// started up, had its index catch up, but never flushed before going down.
// The end-of-sync Commit() runs at the chain tip but m_last_flushed_block
// is null, so it is skipped.
sync_index(false, tip_height, 0);
// Part 2: Restart cleanly. Sync, force a chainstate flush, and drain the
// validation queue so the index's ChainStateFlushed callback runs.
// Now m_last_flushed_block == tip and the index can commit.
sync_index(true, tip_height, tip_height);
// Part 3: Connect a new block on the chain without flushing
// (m_last_flushed_block stays at tip_height). For a real node this would
// happen in parallel with Sync(). Here we do it before Sync() to make the
// race state deterministic.
CreateAndProcessBlock({}, CScript() << OP_TRUE);
sync_index(false, tip_height + 1, tip_height);
}
}
// Test shutdown between BlockConnected and ChainStateFlushed notifications,
// make sure index is not corrupted and reloads at the last committed height.
BOOST_FIXTURE_TEST_CASE(index_unclean_shutdown, TestChain100Setup)
{
Chainstate& chainstate = Assert(m_node.chainman)->ActiveChainstate();
const CChainParams& params = Params();
const int tip_height{WITH_LOCK(cs_main, return chainstate.m_chain.Height())};
chainstate.ForceFlushStateToDisk();
// Drain the notification before registering any index.
m_node.chain->context()->validation_signals->SyncWithValidationInterfaceQueue();
for (const auto& [index_name, make_index] : INDEX_FACTORIES) {
BOOST_TEST_INFO_SCOPE(index_name);
{
auto index{make_index(m_node)};
BOOST_REQUIRE(index->Init());
index->Sync();
std::shared_ptr<const CBlock> new_block;
CBlockIndex* new_block_index = nullptr;
{
const CScript script_pub_key{CScript() << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG};
const CBlock block = this->CreateBlock({}, script_pub_key);
new_block = std::make_shared<CBlock>(block);
LOCK(cs_main);
BlockValidationState state;
BOOST_CHECK(CheckBlock(block, state, params.GetConsensus()));
BOOST_CHECK(m_node.chainman->AcceptBlock(new_block, state, &new_block_index, true, nullptr, nullptr, true));
CCoinsViewCache view(&chainstate.CoinsTip());
BOOST_CHECK(chainstate.ConnectBlock(block, state, new_block_index, view));
}
// Send block connected notification, then stop the index without
// sending a chainstate flushed notification. Prior to #24138, this
// would cause the index to be corrupted and fail to reload.
ValidationInterfaceTest::BlockConnected(ChainstateRole{}, *index, new_block, new_block_index);
index->Stop();
}
{
auto index{make_index(m_node)};
BOOST_REQUIRE(index->Init());
// Make sure the index reloads from the pre-crash commit.
BOOST_CHECK_EQUAL(index->GetSummary().best_block_height, tip_height);
BOOST_REQUIRE(index->StartBackgroundSync());
index->Stop();
}
}
}
class IndexReorgCrash : public BaseIndex
{
private:
FakeNodeClock& m_clock;
std::unique_ptr<BaseIndex::DB> m_db;
std::shared_future<void> m_blocker;
int m_blocking_height;
public:
explicit IndexReorgCrash(std::unique_ptr<interfaces::Chain> chain, std::shared_future<void> blocker, int blocking_height, FakeNodeClock& clock)
: BaseIndex(std::move(chain), "test index", "testidx"), m_clock(clock), m_blocker(blocker), m_blocking_height(blocking_height)
{
const fs::path path = gArgs.GetDataDirNet() / "index";
fs::create_directories(path);
m_db = std::make_unique<BaseIndex::DB>(path / "db", /*n_cache_size=*/0, /*f_memory=*/true, /*f_wipe=*/false);
}
bool AllowPrune() const override { return false; }
BaseIndex::DB& GetDB() const override { return *m_db; }
bool CustomAppend(const interfaces::BlockInfo& block) override
{
// Simulate a delay so new blocks can get connected during the initial sync
if (block.height == m_blocking_height) m_blocker.wait();
// Move mock time forward so the best index gets updated only when we are not at the blocking height
if (block.height == m_blocking_height - 1 || block.height > m_blocking_height) {
m_clock += 31s;
}
return true;
}
};
BOOST_FIXTURE_TEST_CASE(index_reorg_crash, TestChain100Setup)
{
std::promise<void> promise;
std::shared_future<void> blocker(promise.get_future());
int blocking_height = WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Tip()->nHeight);
IndexReorgCrash index{interfaces::MakeChain(m_node), blocker, blocking_height, m_clock};
BOOST_REQUIRE(index.Init());
BOOST_REQUIRE(index.StartBackgroundSync());
auto func_wait_until = [&](int height, std::chrono::milliseconds timeout) {
auto deadline = std::chrono::steady_clock::now() + timeout;
while (index.GetSummary().best_block_height < height) {
if (std::chrono::steady_clock::now() > deadline) {
BOOST_FAIL(strprintf("Timeout waiting for index height %d (current: %d)", height, index.GetSummary().best_block_height));
return;
}
std::this_thread::sleep_for(100ms);
}
};
// Wait until the index is one block before the fork point
func_wait_until(blocking_height - 1, /*timeout=*/5s);
// Create a fork to trigger the reorg
std::vector<std::shared_ptr<CBlock>> fork;
const CBlockIndex* prev_tip = WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Tip()->pprev);
BOOST_REQUIRE(BuildChain(m_node, prev_tip, GetScriptForDestination(PKHash(GenerateRandomKey().GetPubKey())), 3, fork));
for (const auto& block : fork) {
BOOST_REQUIRE(m_node.chainman->ProcessNewBlock(block, /*force_processing=*/true, /*min_pow_checked=*/true, nullptr));
}
// The index thread is blocked and not done
BOOST_CHECK(!index.GetSummary().synced);
// Unblock the index thread so it can process the reorg
promise.set_value();
// Wait for the index to reach the new tip
func_wait_until(blocking_height + 2, 5s);
// Drain unused BlockConnected events, to avoid unsafe memory races during destruction
m_node.chain->context()->validation_signals->SyncWithValidationInterfaceQueue();
// shutdown sequence (c.f. Shutdown() in init.cpp)
index.Stop();
}
BOOST_AUTO_TEST_SUITE_END()