Skip to content

Commit 2501576

Browse files
committed
rpc, index: Add verbose amounts tracking to Coinstats index
1 parent 655d929 commit 2501576

File tree

6 files changed

+181
-10
lines changed

6 files changed

+181
-10
lines changed

src/index/coinstatsindex.cpp

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,31 @@ struct DBVal {
2323
uint64_t transaction_output_count;
2424
uint64_t bogo_size;
2525
CAmount total_amount;
26+
CAmount total_subsidy;
27+
CAmount block_unspendable_amount;
28+
CAmount block_prevout_spent_amount;
29+
CAmount block_new_outputs_ex_coinbase_amount;
30+
CAmount block_coinbase_amount;
31+
CAmount unspendables_genesis_block;
32+
CAmount unspendables_bip30;
33+
CAmount unspendables_scripts;
34+
CAmount unspendables_unclaimed_rewards;
2635

2736
SERIALIZE_METHODS(DBVal, obj)
2837
{
2938
READWRITE(obj.muhash);
3039
READWRITE(obj.transaction_output_count);
3140
READWRITE(obj.bogo_size);
3241
READWRITE(obj.total_amount);
42+
READWRITE(obj.total_subsidy);
43+
READWRITE(obj.block_unspendable_amount);
44+
READWRITE(obj.block_prevout_spent_amount);
45+
READWRITE(obj.block_new_outputs_ex_coinbase_amount);
46+
READWRITE(obj.block_coinbase_amount);
47+
READWRITE(obj.unspendables_genesis_block);
48+
READWRITE(obj.unspendables_bip30);
49+
READWRITE(obj.unspendables_scripts);
50+
READWRITE(obj.unspendables_unclaimed_rewards);
3351
}
3452
};
3553

@@ -88,6 +106,8 @@ CoinStatsIndex::CoinStatsIndex(size_t n_cache_size, bool f_memory, bool f_wipe)
88106
bool CoinStatsIndex::WriteBlock(const CBlock& block, const CBlockIndex* pindex)
89107
{
90108
CBlockUndo block_undo;
109+
const CAmount block_subsidy{GetBlockSubsidy(pindex->nHeight, Params().GetConsensus())};
110+
m_total_subsidy += block_subsidy;
91111

92112
// Ignore genesis block
93113
if (pindex->nHeight > 0) {
@@ -118,6 +138,8 @@ bool CoinStatsIndex::WriteBlock(const CBlock& block, const CBlockIndex* pindex)
118138

119139
// Skip duplicate txid coinbase transactions (BIP30).
120140
if (is_bip30_block && tx->IsCoinBase()) {
141+
m_block_unspendable_amount += block_subsidy;
142+
m_unspendables_bip30 += block_subsidy;
121143
continue;
122144
}
123145

@@ -127,10 +149,20 @@ bool CoinStatsIndex::WriteBlock(const CBlock& block, const CBlockIndex* pindex)
127149
COutPoint outpoint{tx->GetHash(), static_cast<uint32_t>(j)};
128150

129151
// Skip unspendable coins
130-
if (coin.out.scriptPubKey.IsUnspendable()) continue;
152+
if (coin.out.scriptPubKey.IsUnspendable()) {
153+
m_block_unspendable_amount += coin.out.nValue;
154+
m_unspendables_scripts += coin.out.nValue;
155+
continue;
156+
}
131157

132158
m_muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin)));
133159

160+
if (tx->IsCoinBase()) {
161+
m_block_coinbase_amount += coin.out.nValue;
162+
} else {
163+
m_block_new_outputs_ex_coinbase_amount += coin.out.nValue;
164+
}
165+
134166
++m_transaction_output_count;
135167
m_total_amount += coin.out.nValue;
136168
m_bogo_size += GetBogoSize(coin.out.scriptPubKey);
@@ -146,19 +178,42 @@ bool CoinStatsIndex::WriteBlock(const CBlock& block, const CBlockIndex* pindex)
146178

147179
m_muhash.Remove(MakeUCharSpan(TxOutSer(outpoint, coin)));
148180

181+
m_block_prevout_spent_amount += coin.out.nValue;
182+
149183
--m_transaction_output_count;
150184
m_total_amount -= coin.out.nValue;
151185
m_bogo_size -= GetBogoSize(coin.out.scriptPubKey);
152186
}
153187
}
154188
}
189+
} else {
190+
// genesis block
191+
m_block_unspendable_amount += block_subsidy;
192+
m_unspendables_genesis_block += block_subsidy;
155193
}
156194

195+
// If spent prevouts + block subsidy are still a higher amount than
196+
// new outputs + coinbase + current unspendable amount this means
197+
// the miner did not claim the full block reward. Unclaimed block
198+
// rewards are also unspendable.
199+
const CAmount unclaimed_rewards{(m_block_prevout_spent_amount + m_total_subsidy) - (m_block_new_outputs_ex_coinbase_amount + m_block_coinbase_amount + m_block_unspendable_amount)};
200+
m_block_unspendable_amount += unclaimed_rewards;
201+
m_unspendables_unclaimed_rewards += unclaimed_rewards;
202+
157203
std::pair<uint256, DBVal> value;
158204
value.first = pindex->GetBlockHash();
159205
value.second.transaction_output_count = m_transaction_output_count;
160206
value.second.bogo_size = m_bogo_size;
161207
value.second.total_amount = m_total_amount;
208+
value.second.total_subsidy = m_total_subsidy;
209+
value.second.block_unspendable_amount = m_block_unspendable_amount;
210+
value.second.block_prevout_spent_amount = m_block_prevout_spent_amount;
211+
value.second.block_new_outputs_ex_coinbase_amount = m_block_new_outputs_ex_coinbase_amount;
212+
value.second.block_coinbase_amount = m_block_coinbase_amount;
213+
value.second.unspendables_genesis_block = m_unspendables_genesis_block;
214+
value.second.unspendables_bip30 = m_unspendables_bip30;
215+
value.second.unspendables_scripts = m_unspendables_scripts;
216+
value.second.unspendables_unclaimed_rewards = m_unspendables_unclaimed_rewards;
162217

163218
uint256 out;
164219
m_muhash.Finalize(out);
@@ -261,6 +316,15 @@ bool CoinStatsIndex::LookUpStats(const CBlockIndex* block_index, CCoinsStats& co
261316
coins_stats.nTransactionOutputs = entry.transaction_output_count;
262317
coins_stats.nBogoSize = entry.bogo_size;
263318
coins_stats.nTotalAmount = entry.total_amount;
319+
coins_stats.total_subsidy = entry.total_subsidy;
320+
coins_stats.block_unspendable_amount = entry.block_unspendable_amount;
321+
coins_stats.block_prevout_spent_amount = entry.block_prevout_spent_amount;
322+
coins_stats.block_new_outputs_ex_coinbase_amount = entry.block_new_outputs_ex_coinbase_amount;
323+
coins_stats.block_coinbase_amount = entry.block_coinbase_amount;
324+
coins_stats.unspendables_genesis_block = entry.unspendables_genesis_block;
325+
coins_stats.unspendables_bip30 = entry.unspendables_bip30;
326+
coins_stats.unspendables_scripts = entry.unspendables_scripts;
327+
coins_stats.unspendables_unclaimed_rewards = entry.unspendables_unclaimed_rewards;
264328

265329
return true;
266330
}
@@ -289,6 +353,15 @@ bool CoinStatsIndex::Init()
289353
m_transaction_output_count = entry.transaction_output_count;
290354
m_bogo_size = entry.bogo_size;
291355
m_total_amount = entry.total_amount;
356+
m_total_subsidy = entry.total_subsidy;
357+
m_block_unspendable_amount = entry.block_unspendable_amount;
358+
m_block_prevout_spent_amount = entry.block_prevout_spent_amount;
359+
m_block_new_outputs_ex_coinbase_amount = entry.block_new_outputs_ex_coinbase_amount;
360+
m_block_coinbase_amount = entry.block_coinbase_amount;
361+
m_unspendables_genesis_block = entry.unspendables_genesis_block;
362+
m_unspendables_bip30 = entry.unspendables_bip30;
363+
m_unspendables_scripts = entry.unspendables_scripts;
364+
m_unspendables_unclaimed_rewards = entry.unspendables_unclaimed_rewards;
292365
}
293366

294367
return true;
@@ -303,6 +376,9 @@ bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex
303376
CBlockUndo block_undo;
304377
std::pair<uint256, DBVal> read_out;
305378

379+
const CAmount block_subsidy{GetBlockSubsidy(pindex->nHeight, Params().GetConsensus())};
380+
m_total_subsidy -= block_subsidy;
381+
306382
// Ignore genesis block
307383
if (pindex->nHeight > 0) {
308384
if (!UndoReadFromDisk(block_undo, pindex)) {
@@ -332,9 +408,23 @@ bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex
332408
Coin coin{out, pindex->nHeight, tx->IsCoinBase()};
333409

334410
// Skip unspendable coins
335-
if (coin.out.scriptPubKey.IsUnspendable()) continue;
411+
if (coin.out.scriptPubKey.IsUnspendable()) {
412+
m_block_unspendable_amount -= coin.out.nValue;
413+
m_unspendables_scripts -= coin.out.nValue;
414+
continue;
415+
}
336416

337417
m_muhash.Remove(MakeUCharSpan(TxOutSer(outpoint, coin)));
418+
419+
if (tx->IsCoinBase()) {
420+
m_block_coinbase_amount -= coin.out.nValue;
421+
} else {
422+
m_block_new_outputs_ex_coinbase_amount -= coin.out.nValue;
423+
}
424+
425+
--m_transaction_output_count;
426+
m_total_amount -= coin.out.nValue;
427+
m_bogo_size -= GetBogoSize(coin.out.scriptPubKey);
338428
}
339429

340430
// The coinbase tx has no undo data since no former output is spent
@@ -346,18 +436,37 @@ bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex
346436
COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n};
347437

348438
m_muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin)));
439+
440+
m_block_prevout_spent_amount -= coin.out.nValue;
441+
442+
m_transaction_output_count++;
443+
m_total_amount += coin.out.nValue;
444+
m_bogo_size += GetBogoSize(coin.out.scriptPubKey);
349445
}
350446
}
351447
}
352448

353-
// Check that the rolled back internal value of muhash is consistent with the DB read out
449+
const CAmount unclaimed_rewards{(m_block_new_outputs_ex_coinbase_amount + m_block_coinbase_amount + m_block_unspendable_amount) - (m_block_prevout_spent_amount + m_total_subsidy)};
450+
m_block_unspendable_amount -= unclaimed_rewards;
451+
m_unspendables_unclaimed_rewards -= unclaimed_rewards;
452+
453+
// Check that the rolled back internal values are consistent with the DB read out
354454
uint256 out;
355455
m_muhash.Finalize(out);
356456
Assert(read_out.second.muhash == out);
357457

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;
458+
Assert(m_transaction_output_count == read_out.second.transaction_output_count);
459+
Assert(m_total_amount == read_out.second.total_amount);
460+
Assert(m_bogo_size == read_out.second.bogo_size);
461+
Assert(m_total_subsidy == read_out.second.total_subsidy);
462+
Assert(m_block_unspendable_amount == read_out.second.block_unspendable_amount);
463+
Assert(m_block_prevout_spent_amount == read_out.second.block_prevout_spent_amount);
464+
Assert(m_block_new_outputs_ex_coinbase_amount == read_out.second.block_new_outputs_ex_coinbase_amount);
465+
Assert(m_block_coinbase_amount == read_out.second.block_coinbase_amount);
466+
Assert(m_unspendables_genesis_block == read_out.second.unspendables_genesis_block);
467+
Assert(m_unspendables_bip30 == read_out.second.unspendables_bip30);
468+
Assert(m_unspendables_scripts == read_out.second.unspendables_scripts);
469+
Assert(m_unspendables_unclaimed_rewards == read_out.second.unspendables_unclaimed_rewards);
361470

362471
return m_db->Write(DB_MUHASH, m_muhash);
363472
}

src/index/coinstatsindex.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ class CoinStatsIndex final : public BaseIndex
2424
uint64_t m_transaction_output_count{0};
2525
uint64_t m_bogo_size{0};
2626
CAmount m_total_amount{0};
27+
CAmount m_total_subsidy{0};
28+
CAmount m_block_unspendable_amount{0};
29+
CAmount m_block_prevout_spent_amount{0};
30+
CAmount m_block_new_outputs_ex_coinbase_amount{0};
31+
CAmount m_block_coinbase_amount{0};
32+
CAmount m_unspendables_genesis_block{0};
33+
CAmount m_unspendables_bip30{0};
34+
CAmount m_unspendables_scripts{0};
35+
CAmount m_unspendables_unclaimed_rewards{0};
2736

2837
bool ReverseBlock(const CBlock& block, const CBlockIndex* pindex);
2938

src/node/coinstats.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ struct CCoinsStats
4141

4242
bool from_index{false};
4343

44+
// Following values are only available from coinstats index
45+
CAmount total_subsidy{0};
46+
CAmount block_unspendable_amount{0};
47+
CAmount block_prevout_spent_amount{0};
48+
CAmount block_new_outputs_ex_coinbase_amount{0};
49+
CAmount block_coinbase_amount{0};
50+
CAmount unspendables_genesis_block{0};
51+
CAmount unspendables_bip30{0};
52+
CAmount unspendables_scripts{0};
53+
CAmount unspendables_unclaimed_rewards{0};
54+
4455
CCoinsStats(CoinStatsHashType hash_type) : m_hash_type(hash_type) {}
4556
};
4657

src/rpc/blockchain.cpp

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,8 +1113,23 @@ static RPCHelpMan gettxoutsetinfo()
11131113
{RPCResult::Type::STR_HEX, "hash_serialized_2", /* optional */ true, "The serialized hash (only present if 'hash_serialized_2' hash_type is chosen)"},
11141114
{RPCResult::Type::STR_HEX, "muhash", /* optional */ true, "The serialized hash (only present if 'muhash' hash_type is chosen)"},
11151115
{RPCResult::Type::NUM, "transactions", "The number of transactions with unspent outputs (not available when coinstatsindex is used)"},
1116-
{RPCResult::Type::NUM, "disk_size", "The estimated size of the chainstate on disk"},
1116+
{RPCResult::Type::NUM, "disk_size", "The estimated size of the chainstate on disk (not available when coinstatsindex is used)"},
11171117
{RPCResult::Type::STR_AMOUNT, "total_amount", "The total amount of coins in the UTXO set"},
1118+
{RPCResult::Type::STR_AMOUNT, "total_unspendable_amount", "The total amount of coins permanently excluded from the UTXO set (only available if coinstatsindex is used)"},
1119+
{RPCResult::Type::OBJ, "block_info", "Info on amounts in the block at this block height (only available if coinstatsindex is used)",
1120+
{
1121+
{RPCResult::Type::STR_AMOUNT, "prevout_spent", ""},
1122+
{RPCResult::Type::STR_AMOUNT, "coinbase", ""},
1123+
{RPCResult::Type::STR_AMOUNT, "new_outputs_ex_coinbase", ""},
1124+
{RPCResult::Type::STR_AMOUNT, "unspendable", ""},
1125+
{RPCResult::Type::OBJ, "unspendables", "Detailed view of the unspendable categories",
1126+
{
1127+
{RPCResult::Type::STR_AMOUNT, "genesis_block", ""},
1128+
{RPCResult::Type::STR_AMOUNT, "bip30", "Transactions overridden by duplicates (no longer possible with BIP30)"},
1129+
{RPCResult::Type::STR_AMOUNT, "scripts", "Amounts sent to scripts that are unspendable (for example OP_RETURN outputs)"},
1130+
{RPCResult::Type::STR_AMOUNT, "unclaimed_rewards", "Fee rewards that miners did not claim in their coinbase transaction"},
1131+
}}
1132+
}},
11181133
}},
11191134
RPCExamples{
11201135
HelpExampleCli("gettxoutsetinfo", "") +
@@ -1130,9 +1145,7 @@ static RPCHelpMan gettxoutsetinfo()
11301145
{
11311146
UniValue ret(UniValue::VOBJ);
11321147

1133-
::ChainstateActive().ForceFlushStateToDisk();
11341148
CBlockIndex* pindex{nullptr};
1135-
11361149
const CoinStatsHashType hash_type{request.params[0].isNull() ? CoinStatsHashType::HASH_SERIALIZED : ParseHashType(request.params[0].get_str())};
11371150
CCoinsStats stats{hash_type};
11381151

@@ -1147,6 +1160,7 @@ static RPCHelpMan gettxoutsetinfo()
11471160
LOCK(::cs_main);
11481161
coins_view = &active_chainstate.CoinsDB();
11491162
blockman = &active_chainstate.m_blockman;
1163+
pindex = blockman->LookupBlockIndex(coins_view->GetBestBlock());
11501164
}
11511165

11521166
if (!request.params[1].isNull()) {
@@ -1168,11 +1182,34 @@ static RPCHelpMan gettxoutsetinfo()
11681182
if (hash_type == CoinStatsHashType::MUHASH) {
11691183
ret.pushKV("muhash", stats.hashSerialized.GetHex());
11701184
}
1185+
ret.pushKV("total_amount", ValueFromAmount(stats.nTotalAmount));
11711186
if (!stats.from_index) {
11721187
ret.pushKV("transactions", static_cast<int64_t>(stats.nTransactions));
11731188
ret.pushKV("disk_size", stats.nDiskSize);
1189+
} else {
1190+
ret.pushKV("total_unspendable_amount", ValueFromAmount(stats.block_unspendable_amount));
1191+
1192+
CCoinsStats prev_stats{hash_type};
1193+
1194+
if (pindex->nHeight > 0) {
1195+
GetUTXOStats(coins_view, WITH_LOCK(::cs_main, return std::ref(g_chainman.m_blockman)), prev_stats, node.rpc_interruption_point, pindex->pprev);
1196+
}
1197+
1198+
UniValue block_info(UniValue::VOBJ);
1199+
block_info.pushKV("prevout_spent", ValueFromAmount(stats.block_prevout_spent_amount - prev_stats.block_prevout_spent_amount));
1200+
block_info.pushKV("coinbase", ValueFromAmount(stats.block_coinbase_amount - prev_stats.block_coinbase_amount));
1201+
block_info.pushKV("new_outputs_ex_coinbase", ValueFromAmount(stats.block_new_outputs_ex_coinbase_amount - prev_stats.block_new_outputs_ex_coinbase_amount));
1202+
block_info.pushKV("unspendable", ValueFromAmount(stats.block_unspendable_amount - prev_stats.block_unspendable_amount));
1203+
1204+
UniValue unspendables(UniValue::VOBJ);
1205+
unspendables.pushKV("genesis_block", ValueFromAmount(stats.unspendables_genesis_block - prev_stats.unspendables_genesis_block));
1206+
unspendables.pushKV("bip30", ValueFromAmount(stats.unspendables_bip30 - prev_stats.unspendables_bip30));
1207+
unspendables.pushKV("scripts", ValueFromAmount(stats.unspendables_scripts - prev_stats.unspendables_scripts));
1208+
unspendables.pushKV("unclaimed_rewards", ValueFromAmount(stats.unspendables_unclaimed_rewards - prev_stats.unspendables_unclaimed_rewards));
1209+
block_info.pushKV("unspendables", unspendables);
1210+
1211+
ret.pushKV("block_info", block_info);
11741212
}
1175-
ret.pushKV("total_amount", ValueFromAmount(stats.nTotalAmount));
11761213
} else {
11771214
if (g_coin_stats_index) {
11781215
const IndexSummary summary{g_coin_stats_index->GetSummary()};

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
127127
{ "gettxout", 1, "n" },
128128
{ "gettxout", 2, "include_mempool" },
129129
{ "gettxoutproof", 0, "txids" },
130+
{ "gettxoutsetinfo", 1, "hash_or_height" },
130131
{ "lockunspent", 0, "unlock" },
131132
{ "lockunspent", 1, "transactions" },
132133
{ "send", 0, "outputs" },

test/functional/feature_coinstatsindex.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ def _test_coin_stats_index(self):
5656
self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash'))
5757
for hash_option in index_hash_options:
5858
res1 = index_node.gettxoutsetinfo(hash_option)
59+
# The fields 'block_info' and 'total_unspendable_amount' only exist on the index
60+
del res1['block_info'], res1['total_unspendable_amount']
5961
res1.pop('muhash', None)
6062

6163
# Everything left should be the same
@@ -70,11 +72,13 @@ def _test_coin_stats_index(self):
7072
for hash_option in index_hash_options:
7173
# Fetch old stats by height
7274
res2 = index_node.gettxoutsetinfo(hash_option, 102)
75+
del res2['block_info'], res2['total_unspendable_amount']
7376
res2.pop('muhash', None)
7477
assert_equal(res0, res2)
7578

7679
# Fetch old stats by hash
7780
res3 = index_node.gettxoutsetinfo(hash_option, res0['bestblock'])
81+
del res3['block_info'], res3['total_unspendable_amount']
7882
res3.pop('muhash', None)
7983
assert_equal(res0, res3)
8084

0 commit comments

Comments
 (0)