Skip to content

Commit dd58a4d

Browse files
committed
index: Add Coinstats index
The index holds the values previously calculated in coinstats.cpp for each block, representing the state of the UTXO set at each height.
1 parent a8a46c4 commit dd58a4d

File tree

6 files changed

+437
-8
lines changed

6 files changed

+437
-8
lines changed

src/Makefile.am

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ BITCOIN_CORE_H = \
152152
i2p.h \
153153
index/base.h \
154154
index/blockfilterindex.h \
155+
index/coinstatsindex.h \
155156
index/disktxpos.h \
156157
index/txindex.h \
157158
indirectmap.h \
@@ -318,6 +319,7 @@ libbitcoin_server_a_SOURCES = \
318319
i2p.cpp \
319320
index/base.cpp \
320321
index/blockfilterindex.cpp \
322+
index/coinstatsindex.cpp \
321323
index/txindex.cpp \
322324
init.cpp \
323325
mapport.cpp \

src/index/base.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ class BaseIndex : public CValidationInterface
8181

8282
void ChainStateFlushed(const CBlockLocator& locator) override;
8383

84+
const CBlockIndex* CurrentIndex() { return m_best_block_index.load(); };
85+
8486
/// Initialize internal state from the database and block index.
8587
virtual bool Init();
8688

src/index/coinstatsindex.cpp

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
// Copyright (c) 2020-2021 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 <chainparams.h>
6+
#include <coins.h>
7+
#include <crypto/muhash.h>
8+
#include <index/coinstatsindex.h>
9+
#include <node/blockstorage.h>
10+
#include <serialize.h>
11+
#include <txdb.h>
12+
#include <undo.h>
13+
#include <validation.h>
14+
15+
static constexpr char DB_BLOCK_HASH = 's';
16+
static constexpr char DB_BLOCK_HEIGHT = 't';
17+
static constexpr char DB_MUHASH = 'M';
18+
19+
namespace {
20+
21+
struct DBVal {
22+
uint256 muhash;
23+
uint64_t transaction_output_count;
24+
uint64_t bogo_size;
25+
CAmount total_amount;
26+
27+
SERIALIZE_METHODS(DBVal, obj)
28+
{
29+
READWRITE(obj.muhash);
30+
READWRITE(obj.transaction_output_count);
31+
READWRITE(obj.bogo_size);
32+
READWRITE(obj.total_amount);
33+
}
34+
};
35+
36+
struct DBHeightKey {
37+
int height;
38+
39+
explicit DBHeightKey(int height_in) : height(height_in) {}
40+
41+
template <typename Stream>
42+
void Serialize(Stream& s) const
43+
{
44+
ser_writedata8(s, DB_BLOCK_HEIGHT);
45+
ser_writedata32be(s, height);
46+
}
47+
48+
template <typename Stream>
49+
void Unserialize(Stream& s)
50+
{
51+
char prefix{static_cast<char>(ser_readdata8(s))};
52+
if (prefix != DB_BLOCK_HEIGHT) {
53+
throw std::ios_base::failure("Invalid format for coinstatsindex DB height key");
54+
}
55+
height = ser_readdata32be(s);
56+
}
57+
};
58+
59+
struct DBHashKey {
60+
uint256 block_hash;
61+
62+
explicit DBHashKey(const uint256& hash_in) : block_hash(hash_in) {}
63+
64+
SERIALIZE_METHODS(DBHashKey, obj)
65+
{
66+
char prefix{DB_BLOCK_HASH};
67+
READWRITE(prefix);
68+
if (prefix != DB_BLOCK_HASH) {
69+
throw std::ios_base::failure("Invalid format for coinstatsindex DB hash key");
70+
}
71+
72+
READWRITE(obj.block_hash);
73+
}
74+
};
75+
76+
}; // namespace
77+
78+
std::unique_ptr<CoinStatsIndex> g_coin_stats_index;
79+
80+
CoinStatsIndex::CoinStatsIndex(size_t n_cache_size, bool f_memory, bool f_wipe)
81+
{
82+
fs::path path{GetDataDir() / "indexes" / "coinstats"};
83+
fs::create_directories(path);
84+
85+
m_db = std::make_unique<CoinStatsIndex::DB>(path / "db", n_cache_size, f_memory, f_wipe);
86+
}
87+
88+
bool CoinStatsIndex::WriteBlock(const CBlock& block, const CBlockIndex* pindex)
89+
{
90+
CBlockUndo block_undo;
91+
92+
// Ignore genesis block
93+
if (pindex->nHeight > 0) {
94+
if (!UndoReadFromDisk(block_undo, pindex)) {
95+
return false;
96+
}
97+
98+
std::pair<uint256, DBVal> read_out;
99+
if (!m_db->Read(DBHeightKey(pindex->nHeight - 1), read_out)) {
100+
return false;
101+
}
102+
103+
uint256 expected_block_hash{pindex->pprev->GetBlockHash()};
104+
if (read_out.first != expected_block_hash) {
105+
if (!m_db->Read(DBHashKey(expected_block_hash), read_out)) {
106+
return error("%s: previous block header belongs to unexpected block %s; expected %s",
107+
__func__, read_out.first.ToString(), expected_block_hash.ToString());
108+
}
109+
}
110+
111+
// TODO: Deduplicate BIP30 related code
112+
bool is_bip30_block{(pindex->nHeight == 91722 && pindex->GetBlockHash() == uint256S("0x00000000000271a2dc26e7667f8419f2e15416dc6955e5a6c6cdf3f2574dd08e")) ||
113+
(pindex->nHeight == 91812 && pindex->GetBlockHash() == uint256S("0x00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2f"))};
114+
115+
// Add the new utxos created from the block
116+
for (size_t i = 0; i < block.vtx.size(); ++i) {
117+
const auto& tx{block.vtx.at(i)};
118+
119+
// Skip duplicate txid coinbase transactions (BIP30).
120+
if (is_bip30_block && tx->IsCoinBase()) {
121+
continue;
122+
}
123+
124+
for (size_t j = 0; j < tx->vout.size(); ++j) {
125+
const CTxOut& out{tx->vout[j]};
126+
Coin coin{out, pindex->nHeight, tx->IsCoinBase()};
127+
COutPoint outpoint{tx->GetHash(), static_cast<uint32_t>(j)};
128+
129+
// Skip unspendable coins
130+
if (coin.out.scriptPubKey.IsUnspendable()) continue;
131+
132+
m_muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin)));
133+
134+
++m_transaction_output_count;
135+
m_total_amount += coin.out.nValue;
136+
m_bogo_size += GetBogoSize(coin.out.scriptPubKey);
137+
}
138+
139+
// The coinbase tx has no undo data since no former output is spent
140+
if (!tx->IsCoinBase()) {
141+
const auto& tx_undo{block_undo.vtxundo.at(i - 1)};
142+
143+
for (size_t j = 0; j < tx_undo.vprevout.size(); ++j) {
144+
Coin coin{tx_undo.vprevout[j]};
145+
COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n};
146+
147+
m_muhash.Remove(MakeUCharSpan(TxOutSer(outpoint, coin)));
148+
149+
--m_transaction_output_count;
150+
m_total_amount -= coin.out.nValue;
151+
m_bogo_size -= GetBogoSize(coin.out.scriptPubKey);
152+
}
153+
}
154+
}
155+
}
156+
157+
std::pair<uint256, DBVal> value;
158+
value.first = pindex->GetBlockHash();
159+
value.second.transaction_output_count = m_transaction_output_count;
160+
value.second.bogo_size = m_bogo_size;
161+
value.second.total_amount = m_total_amount;
162+
163+
uint256 out;
164+
m_muhash.Finalize(out);
165+
value.second.muhash = out;
166+
167+
return m_db->Write(DBHeightKey(pindex->nHeight), value) && m_db->Write(DB_MUHASH, m_muhash);
168+
}
169+
170+
static bool CopyHeightIndexToHashIndex(CDBIterator& db_it, CDBBatch& batch,
171+
const std::string& index_name,
172+
int start_height, int stop_height)
173+
{
174+
DBHeightKey key{start_height};
175+
db_it.Seek(key);
176+
177+
for (int height = start_height; height <= stop_height; ++height) {
178+
if (!db_it.GetKey(key) || key.height != height) {
179+
return error("%s: unexpected key in %s: expected (%c, %d)",
180+
__func__, index_name, DB_BLOCK_HEIGHT, height);
181+
}
182+
183+
std::pair<uint256, DBVal> value;
184+
if (!db_it.GetValue(value)) {
185+
return error("%s: unable to read value in %s at key (%c, %d)",
186+
__func__, index_name, DB_BLOCK_HEIGHT, height);
187+
}
188+
189+
batch.Write(DBHashKey(value.first), std::move(value.second));
190+
191+
db_it.Next();
192+
}
193+
return true;
194+
}
195+
196+
bool CoinStatsIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip)
197+
{
198+
assert(current_tip->GetAncestor(new_tip->nHeight) == new_tip);
199+
200+
CDBBatch batch(*m_db);
201+
std::unique_ptr<CDBIterator> db_it(m_db->NewIterator());
202+
203+
// During a reorg, we need to copy all hash digests for blocks that are
204+
// getting disconnected from the height index to the hash index so we can
205+
// still find them when the height index entries are overwritten.
206+
if (!CopyHeightIndexToHashIndex(*db_it, batch, m_name, new_tip->nHeight, current_tip->nHeight)) {
207+
return false;
208+
}
209+
210+
if (!m_db->WriteBatch(batch)) return false;
211+
212+
{
213+
LOCK(cs_main);
214+
CBlockIndex* iter_tip{g_chainman.m_blockman.LookupBlockIndex(current_tip->GetBlockHash())};
215+
const auto& consensus_params{Params().GetConsensus()};
216+
217+
do {
218+
CBlock block;
219+
220+
if (!ReadBlockFromDisk(block, iter_tip, consensus_params)) {
221+
return error("%s: Failed to read block %s from disk",
222+
__func__, iter_tip->GetBlockHash().ToString());
223+
}
224+
225+
ReverseBlock(block, iter_tip);
226+
227+
iter_tip = iter_tip->GetAncestor(iter_tip->nHeight - 1);
228+
} while (new_tip != iter_tip);
229+
}
230+
231+
return BaseIndex::Rewind(current_tip, new_tip);
232+
}
233+
234+
static bool LookUpOne(const CDBWrapper& db, const CBlockIndex* block_index, DBVal& result)
235+
{
236+
// First check if the result is stored under the height index and the value
237+
// there matches the block hash. This should be the case if the block is on
238+
// the active chain.
239+
std::pair<uint256, DBVal> read_out;
240+
if (!db.Read(DBHeightKey(block_index->nHeight), read_out)) {
241+
return false;
242+
}
243+
if (read_out.first == block_index->GetBlockHash()) {
244+
result = std::move(read_out.second);
245+
return true;
246+
}
247+
248+
// If value at the height index corresponds to an different block, the
249+
// result will be stored in the hash index.
250+
return db.Read(DBHashKey(block_index->GetBlockHash()), result);
251+
}
252+
253+
bool CoinStatsIndex::LookUpStats(const CBlockIndex* block_index, CCoinsStats& coins_stats) const
254+
{
255+
DBVal entry;
256+
if (!LookUpOne(*m_db, block_index, entry)) {
257+
return false;
258+
}
259+
260+
coins_stats.hashSerialized = entry.muhash;
261+
coins_stats.nTransactionOutputs = entry.transaction_output_count;
262+
coins_stats.nBogoSize = entry.bogo_size;
263+
coins_stats.nTotalAmount = entry.total_amount;
264+
265+
return true;
266+
}
267+
268+
bool CoinStatsIndex::Init()
269+
{
270+
if (!m_db->Read(DB_MUHASH, m_muhash)) {
271+
// Check that the cause of the read failure is that the key does not
272+
// exist. Any other errors indicate database corruption or a disk
273+
// failure, and starting the index would cause further corruption.
274+
if (m_db->Exists(DB_MUHASH)) {
275+
return error("%s: Cannot read current %s state; index may be corrupted",
276+
__func__, GetName());
277+
}
278+
}
279+
280+
if (BaseIndex::Init()) {
281+
const CBlockIndex* pindex{CurrentIndex()};
282+
283+
if (pindex) {
284+
DBVal entry;
285+
if (!LookUpOne(*m_db, pindex, entry)) {
286+
return false;
287+
}
288+
289+
m_transaction_output_count = entry.transaction_output_count;
290+
m_bogo_size = entry.bogo_size;
291+
m_total_amount = entry.total_amount;
292+
}
293+
294+
return true;
295+
}
296+
297+
return false;
298+
}
299+
300+
// Reverse a single block as part of a reorg
301+
bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex)
302+
{
303+
CBlockUndo block_undo;
304+
std::pair<uint256, DBVal> read_out;
305+
306+
// Ignore genesis block
307+
if (pindex->nHeight > 0) {
308+
if (!UndoReadFromDisk(block_undo, pindex)) {
309+
return false;
310+
}
311+
312+
if (!m_db->Read(DBHeightKey(pindex->nHeight - 1), read_out)) {
313+
return false;
314+
}
315+
316+
uint256 expected_block_hash{pindex->pprev->GetBlockHash()};
317+
if (read_out.first != expected_block_hash) {
318+
if (!m_db->Read(DBHashKey(expected_block_hash), read_out)) {
319+
return error("%s: previous block header belongs to unexpected block %s; expected %s",
320+
__func__, read_out.first.ToString(), expected_block_hash.ToString());
321+
}
322+
}
323+
}
324+
325+
// Remove the new UTXOs that were created from the block
326+
for (size_t i = 0; i < block.vtx.size(); ++i) {
327+
const auto& tx{block.vtx.at(i)};
328+
329+
for (size_t j = 0; j < tx->vout.size(); ++j) {
330+
const CTxOut& out{tx->vout[j]};
331+
COutPoint outpoint{tx->GetHash(), static_cast<uint32_t>(j)};
332+
Coin coin{out, pindex->nHeight, tx->IsCoinBase()};
333+
334+
// Skip unspendable coins
335+
if (coin.out.scriptPubKey.IsUnspendable()) continue;
336+
337+
m_muhash.Remove(MakeUCharSpan(TxOutSer(outpoint, coin)));
338+
}
339+
340+
// The coinbase tx has no undo data since no former output is spent
341+
if (!tx->IsCoinBase()) {
342+
const auto& tx_undo{block_undo.vtxundo.at(i - 1)};
343+
344+
for (size_t j = 0; j < tx_undo.vprevout.size(); ++j) {
345+
Coin coin{tx_undo.vprevout[j]};
346+
COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n};
347+
348+
m_muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin)));
349+
}
350+
}
351+
}
352+
353+
// Check that the rolled back internal value of muhash is consistent with the DB read out
354+
uint256 out;
355+
m_muhash.Finalize(out);
356+
Assert(read_out.second.muhash == out);
357+
358+
m_transaction_output_count = read_out.second.transaction_output_count;
359+
m_total_amount = read_out.second.total_amount;
360+
m_bogo_size = read_out.second.bogo_size;
361+
362+
return m_db->Write(DB_MUHASH, m_muhash);
363+
}

0 commit comments

Comments
 (0)