Repository navigation
Expand file tree
/
Copy pathbaseindex_tests.cpp
More file actions
328 lines (301 loc) · 14.4 KB
/
Copy pathbaseindex_tests.cpp
File metadata and controls
328 lines (301 loc) · 14.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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
// 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 <chain.h>
#include <consensus/validation.h>
#include <index/coinstatsindex.h>
#include <interfaces/chain.h>
#include <node/blockstorage.h>
#include <script/script.h>
#include <test/util/index.h>
#include <test/util/setup_common.h>
#include <util/check.h>
#include <util/system.h>
#include <validation.h>
#include <validationinterface.h>
#include <boost/test/unit_test.hpp>
#include <memory>
#include <optional>
#include <vector>
namespace {
//! A minimal index that, like Dash's address and spent indexes, keeps one entry per block and erases the entries of
//! rewound blocks in CustomRewind() block by block. The rewind is exposed, and can stop after a given block, so a test
//! can leave the on-disk state that an unclean shutdown in the middle of a reorg would leave, without having to time
//! an actual crash.
class ErasingIndex final : public BaseIndex
{
const std::unique_ptr<BaseIndex::DB> m_db;
const CBlockIndex* m_stop_after{nullptr};
public:
explicit ErasingIndex(std::unique_ptr<interfaces::Chain> chain)
: BaseIndex(std::move(chain), "erasing test index"),
m_db{std::make_unique<BaseIndex::DB>(gArgs.GetDataDirNet() / "indexes" / "erasingtest", /*n_cache_size=*/1 << 20)}
{
}
~ErasingIndex() override
{
Interrupt();
Stop();
}
bool Indexed(const CBlockIndex* block) const { return Indexed(block->GetBlockHash()); }
bool Indexed(const uint256& block_hash) const { return m_db->Exists(std::make_pair(uint8_t{'e'}, block_hash)); }
//! Write the entry CustomAppend() writes, for a block the block index doesn't have
void WriteEntry(const uint256& block_hash)
{
BOOST_REQUIRE(m_db->Write(std::make_pair(uint8_t{'e'}, block_hash), true));
}
//! Rewind, or with stop_after set, stop as a crash would once that block's batch is written
bool RewindTo(const CBlockIndex* current_tip, const CBlockIndex* new_tip, const CBlockIndex* stop_after = nullptr)
{
m_stop_after = stop_after;
const bool ok{CustomRewind({current_tip->GetBlockHash(), current_tip->nHeight}, {new_tip->GetBlockHash(), new_tip->nHeight})};
m_stop_after = nullptr;
return ok;
}
std::optional<uint256> UncommittedTip() const { return m_db->ReadUncommittedTip(); }
void SetUncommittedTip(const uint256& block_hash)
{
CDBBatch batch(*m_db);
m_db->WriteUncommittedTip(batch, block_hash);
m_db->WriteBatch(batch);
}
uint256 CommittedTip() const
{
const auto locator{m_db->ReadBestBlock()};
BOOST_REQUIRE(!locator.IsNull());
return locator.vHave.front();
}
protected:
bool CustomAppend(const interfaces::BlockInfo& block) override { return m_db->Write(std::make_pair(uint8_t{'e'}, block.hash), true); }
bool CustomRewind(const interfaces::BlockKey& current_tip, const interfaces::BlockKey& new_tip) override
{
const CBlockIndex* current_tip_index{WITH_LOCK(::cs_main, return m_chainstate->m_blockman.LookupBlockIndex(current_tip.hash))};
const CBlockIndex* new_tip_index{WITH_LOCK(::cs_main, return m_chainstate->m_blockman.LookupBlockIndex(new_tip.hash))};
return RewindBlockByBlock(current_tip_index, new_tip_index, [&](const CBlockIndex* pindex, CDBBatch& batch) {
if (m_stop_after && pindex == m_stop_after->pprev) return false;
batch.Erase(std::make_pair(uint8_t{'e'}, pindex->GetBlockHash()));
return true;
});
}
BaseIndex::DB& GetDB() const override { return *m_db; }
bool AllowPrune() const override { return true; }
bool WritesEagerly() const override { return true; }
};
const CBlockIndex* AtHeight(ChainstateManager& chainman, int height)
{
return WITH_LOCK(::cs_main, return chainman.ActiveChain()[height]);
}
//! Make `height` the chainstate's last flushed block while the tip stays above it: flush with the blocks above
//! disconnected, then connect them again without a flush.
void LowerLastFlushedBlock(ChainstateManager& chainman, int height)
{
Chainstate& chainstate{chainman.ActiveChainstate()};
CBlockIndex* first_disconnected{WITH_LOCK(::cs_main, return chainman.ActiveChain()[height + 1])};
BlockValidationState state;
BOOST_REQUIRE(chainstate.InvalidateBlock(state, first_disconnected));
chainstate.ForceFlushStateToDisk();
WITH_LOCK(::cs_main, chainstate.ResetBlockFailureFlags(first_disconnected));
BOOST_REQUIRE(chainstate.ActivateBestChain(state));
BOOST_REQUIRE_EQUAL(WITH_LOCK(::cs_main, return chainstate.GetLastFlushedBlock()->nHeight), height);
}
} // namespace
// Tests of generic BaseIndex functionality that is independent of which
// concrete index is being used. CoinStatsIndex is used here merely as a
// convenient instantiation of BaseIndex.
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();
auto sync_index = [&](bool do_flush, int expected_sync_height, int expected_commit_height) {
{
CoinStatsIndex index{interfaces::MakeChain(m_node), /*n_cache_size=*/1 << 20};
BOOST_REQUIRE(index.Start());
IndexWaitSynced(index);
if (do_flush) {
chainstate.ForceFlushStateToDisk();
SyncWithValidationInterfaceQueue();
}
BOOST_CHECK_EQUAL(index.GetSummary().best_block_height, expected_sync_height);
index.Stop();
}
// Reload index to see which block data was actually committed. Init() and Sync() are not
// callable on their own in Dash, so hold back the sync thread while the loaded best block is read.
node::g_indexes_ready_to_sync = false;
{
CoinStatsIndex index{interfaces::MakeChain(m_node), /*n_cache_size=*/1 << 20};
BOOST_REQUIRE(index.Start());
BOOST_CHECK_EQUAL(index.GetSummary().best_block_height, expected_commit_height);
index.Interrupt();
index.Stop();
}
node::g_indexes_ready_to_sync = true;
};
// 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 chain tip (height 100) but
// m_last_flushed_block is null, so it is skipped.
sync_index(false, 100, 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 == 100 and the index can commit.
sync_index(true, 100, 100);
// Part 3: Connect a new block on the chain without flushing
// (m_last_flushed_block stays at 100). 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, 101, 100);
}
// A rewind persists its erasures right away. If the node then stops before the rewound tip is flushed and the
// rewound blocks are back on the active chain, the next start must index them again instead of resuming above them.
BOOST_FIXTURE_TEST_CASE(restart_after_rewind_reindexes_erased_blocks, TestChain100Setup)
{
ChainstateManager& chainman{*Assert(m_node.chainman)};
{
ErasingIndex index{interfaces::MakeChain(m_node)};
BOOST_REQUIRE(index.Start());
IndexWaitSynced(index);
chainman.ActiveChainstate().ForceFlushStateToDisk();
SyncWithValidationInterfaceQueue();
index.Stop();
}
// The rewind target (97) is now above the last flush (95), so Commit() cannot persist it
LowerLastFlushedBlock(chainman, 95);
const CBlockIndex* tip{AtHeight(chainman, 100)};
node::g_indexes_ready_to_sync = false;
{
ErasingIndex index{interfaces::MakeChain(m_node)};
BOOST_REQUIRE(index.Start());
BOOST_CHECK(index.CommittedTip() == tip->GetBlockHash());
BOOST_REQUIRE(index.RewindTo(tip, AtHeight(chainman, 97)));
BOOST_CHECK(!index.Indexed(tip));
BOOST_CHECK(index.CommittedTip() == AtHeight(chainman, 97)->GetBlockHash());
index.Interrupt();
index.Stop();
}
node::g_indexes_ready_to_sync = true;
// The rewound blocks are still the active chain, as after a restart in which their branch wins again
ErasingIndex index{interfaces::MakeChain(m_node)};
BOOST_REQUIRE(index.Start());
IndexWaitSynced(index);
for (int height{98}; height <= 100; ++height) {
BOOST_CHECK_MESSAGE(index.Indexed(AtHeight(chainman, height)), "block " << height << " was not indexed again");
}
index.Stop();
}
// Each block's erasures are written in one batch with a locator at its parent. A node that stops in the middle of a
// rewind then resumes from the last block whose entries are still there, and indexes the erased ones again.
BOOST_FIXTURE_TEST_CASE(interrupted_rewind_keeps_locator_below_erased_entries, TestChain100Setup)
{
ChainstateManager& chainman{*Assert(m_node.chainman)};
{
ErasingIndex index{interfaces::MakeChain(m_node)};
BOOST_REQUIRE(index.Start());
IndexWaitSynced(index);
chainman.ActiveChainstate().ForceFlushStateToDisk();
SyncWithValidationInterfaceQueue();
index.Stop();
}
const CBlockIndex* tip{AtHeight(chainman, 100)};
node::g_indexes_ready_to_sync = false;
{
ErasingIndex index{interfaces::MakeChain(m_node)};
BOOST_REQUIRE(index.Start());
BOOST_CHECK(index.CommittedTip() == tip->GetBlockHash());
BOOST_CHECK(!index.RewindTo(tip, AtHeight(chainman, 97), /*stop_after=*/tip));
BOOST_CHECK(!index.Indexed(tip));
BOOST_CHECK(index.Indexed(AtHeight(chainman, 99)));
BOOST_CHECK(index.CommittedTip() == AtHeight(chainman, 99)->GetBlockHash());
index.Interrupt();
index.Stop();
}
node::g_indexes_ready_to_sync = true;
ErasingIndex index{interfaces::MakeChain(m_node)};
BOOST_REQUIRE(index.Start());
IndexWaitSynced(index);
BOOST_CHECK_MESSAGE(index.Indexed(tip), "block 100 was not indexed again");
index.Stop();
}
// A sync that ends above the last flush can't commit its locator, but its entries are on disk. If those blocks leave
// the active chain while the index is off, the next start must rewind them from the tip the sync reached.
BOOST_FIXTURE_TEST_CASE(restart_rewinds_blocks_synced_above_flush, TestChain100Setup)
{
ChainstateManager& chainman{*Assert(m_node.chainman)};
LowerLastFlushedBlock(chainman, 97);
const std::vector<const CBlockIndex*> orphaned{AtHeight(chainman, 98), AtHeight(chainman, 99), AtHeight(chainman, 100)};
{
ErasingIndex index{interfaces::MakeChain(m_node)};
BOOST_REQUIRE(index.Start());
IndexWaitSynced(index);
BOOST_CHECK(index.UncommittedTip() == orphaned.back()->GetBlockHash());
index.Stop();
}
// A rewind that stops after its first block moves the uncommitted tip down with it, so the next start, with the
// rewound block still on the active chain, indexes it again
node::g_indexes_ready_to_sync = false;
{
ErasingIndex index{interfaces::MakeChain(m_node)};
BOOST_REQUIRE(index.Start());
BOOST_CHECK(!index.RewindTo(orphaned.back(), AtHeight(chainman, 97), /*stop_after=*/orphaned.back()));
BOOST_CHECK(index.UncommittedTip() == orphaned[1]->GetBlockHash());
index.Interrupt();
index.Stop();
}
node::g_indexes_ready_to_sync = true;
{
ErasingIndex index{interfaces::MakeChain(m_node)};
BOOST_REQUIRE(index.Start());
IndexWaitSynced(index);
BOOST_CHECK(index.Indexed(orphaned.back()));
index.Stop();
}
// The reorged-away blocks stay in the block index, as after a periodic write before an unclean stop
BlockValidationState state;
BOOST_REQUIRE(chainman.ActiveChainstate().InvalidateBlock(state, WITH_LOCK(::cs_main, return chainman.ActiveChain()[98])));
for (int i{0}; i < 4; ++i) CreateAndProcessBlock({}, CScript() << OP_TRUE);
{
ErasingIndex index{interfaces::MakeChain(m_node)};
BOOST_REQUIRE(index.Start());
IndexWaitSynced(index);
for (const CBlockIndex* block : orphaned) {
BOOST_CHECK_MESSAGE(!index.Indexed(block), "orphaned block " << block->nHeight << " is still indexed");
}
for (int height{98}; height <= 101; ++height) {
BOOST_CHECK(index.Indexed(AtHeight(chainman, height)));
}
chainman.ActiveChainstate().ForceFlushStateToDisk();
SyncWithValidationInterfaceQueue();
BOOST_CHECK(!index.UncommittedTip());
index.Stop();
}
// An uncommitted tip that isn't above the locator was left behind by an older binary: it is dropped and the index
// starts from the locator
node::g_indexes_ready_to_sync = false;
{
ErasingIndex index{interfaces::MakeChain(m_node)};
index.SetUncommittedTip(orphaned.back()->GetBlockHash());
BOOST_REQUIRE(index.Start());
BOOST_CHECK_EQUAL(index.GetSummary().best_block_height, 101);
BOOST_CHECK(!index.UncommittedTip());
index.Interrupt();
index.Stop();
}
// An unclean stop can lose blocks the index already wrote entries for from the block index. Startup can't rewind
// those entries, so it must fail rather than resume from the locator with them in place.
const CBlock lost_block{CreateBlock({}, CScript() << OP_TRUE, chainman.ActiveChainstate())};
BOOST_REQUIRE(!WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(lost_block.GetHash())));
{
ErasingIndex index{interfaces::MakeChain(m_node)};
index.WriteEntry(lost_block.GetHash());
index.SetUncommittedTip(lost_block.GetHash());
BOOST_CHECK(!index.Start());
BOOST_CHECK(index.UncommittedTip() == lost_block.GetHash());
BOOST_CHECK(index.Indexed(lost_block.GetHash()));
}
node::g_indexes_ready_to_sync = true;
}
BOOST_AUTO_TEST_SUITE_END()