Requires #19521 (this PR only adds the last commit).
The rpc dumpcoinstats dumps the content of the coinstats index into a CSV file for auditing purposes.
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--021abf342d371248e50ceaed478a90ca-->
See the guideline for information on the review process.
| Type | Reviewers |
|---|---|
| Concept ACK | laanwj, promag |
| Approach ACK | Zero-1729 |
| Stale ACK | PierreRochard, w0xlt, Sjors |
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
<!--174a7506f384e20aa4161008e828411d-->
Reviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
Tested ACK 21c42c22c9b50f60a4b4a0ee84c45f47df98752f
Concept ACK, I wish we had gotten streaming to work though so that the file can be downloaded (see my experiment in #7759). RPCs that creates a server-local file, although there are already some others, are not that great from a security and usability point of view.
Concept ACK, could have a functional test.
Added functional test.
Rebased
Error: RPC command "dumpcoinstats" not found in RPC_COMMANDS_SAFE_FOR_FUZZING or RPC_COMMANDS_NOT_SAFE_FOR_FUZZING. Please update test/fuzz/rpc.cpp.
Thanks @MarcoFalke , fixed!
Approach ACK b85813a
Rebased and fixed silent merge conflict.
1249 | + CHECK_NONFATAL(stats.total_amount.has_value()); 1250 | + file << strprintf("%s,", FormatMoney(stats.total_amount.value())); 1251 | + file << strprintf("%s,", FormatMoney(stats.total_unspendable_amount)); 1252 | + file << strprintf("%s,", FormatMoney(stats.total_prevout_spent_amount)); 1253 | + file << strprintf("%s,", FormatMoney(stats.total_new_outputs_ex_coinbase_amount)); 1254 | + file << strprintf("%s,", FormatMoney(stats.total_coinbase_amount));
I would prefer these to be per-block amounts like in the RPC (obtaining the sum of a column is trivial in a spreadsheet).
There seemed to be a few people that preferred this version back when I first built the coinstatsindex but we can just have both. So now there is a bool argument to switch between cummulative and per-block.
Concept ACK. Tested and reviewed 87fd7b8, but see inline
Also, not to scope creep, but can you add a few goodies from CBlockIndex to the CSV?
nTx)nChainWork (or per block)nVersionAlso, not to scope creep, but can you add a few goodies from
CBlockIndexto the CSV?
- the number of transactions per block (
nTx)- timestamp (and maybe MTP, but that can be derived in a spreadsheet)
nChainWork(or per block)nVersion
Easy enough, if it enables more use cases we can easily add more. I have added all of these.
1172 | +{ 1173 | + return RPCHelpMan{"dumpcoinstats", 1174 | + "\nDumps content of coinstats index to a CSV file.\n", 1175 | + { 1176 | + {"filename", RPCArg::Type::STR, RPCArg::Optional::NO, "The filename with path (absolute path recommended)"}, 1177 | + {"cummulative", RPCArg::Type::BOOL, RPCArg::Default{false}, "The numbers coming out of coinstatsindex will be cummulative"},
cumulative
fixed
1269 | + file << strprintf("%s,", FormatMoney(stats.total_unspendables_genesis_block)); 1270 | + file << strprintf("%s,", FormatMoney(stats.total_unspendables_bip30)); 1271 | + file << strprintf("%s,", FormatMoney(stats.total_unspendables_scripts)); 1272 | + file << strprintf("%s,", FormatMoney(stats.total_unspendables_unclaimed_rewards)); 1273 | + } else { 1274 | + file << strprintf("%s,", FormatMoney(stats.total_unspendable_amount - prev_stats.total_unspendable_amount));
Nit: maybe calculate these values before the if (cummulative) to remove the repetition here:
const unspendable_amount = tats.total_unspendable_amount - cumulative ? 0 : prev_stats.total_prevout_spent_amount
...
file << strprintf("%s,", FormatMoney(unspendable_amount));
done
1208 | + } 1209 | + 1210 | + const bool cummulative{request.params[1].isNull() ? false : request.params[1].get_bool()}; 1211 | + 1212 | + file << strprintf("height," 1213 | + "bestblock,"
block_hash ?
done
1212 | + file << strprintf("height," 1213 | + "bestblock," 1214 | + "txouts," 1215 | + "bogosize," 1216 | + "muhash," 1217 | + "total_amount,"
cumulative ? "total_amount" : "amount"
I was a bit hesitant about this because it might make some scripting harder if the header is different but I guess it makes sense to change it if the values are different. So done now.
f73b68c262df256e08a32af4a2e8142b3f68dee6 almost there
154 | @@ -155,6 +155,7 @@ static const CRPCConvertParam vRPCConvertParams[] = 155 | { "getblockstats", 0, "hash_or_height" }, 156 | { "getblockstats", 1, "stats" }, 157 | { "pruneblockchain", 0, "height" }, 158 | + { "dumpcoinstats", 1, "cummulative"},
Missed a spot
Gah, thought I had grepped for it. But there was another silent merge conflict anyway... :/
Windows CI seems unhappy:
PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'C:\Users\ContainerAdministrator\AppData\Local\Temp\test_runner_₿_🏃_20220207_231436\feature_coinstatsindex_17\node1\test.csv'
1162 | @@ -1161,6 +1163,162 @@ static RPCHelpMan pruneblockchain() 1163 | }; 1164 | } 1165 | 1166 | +static RPCHelpMan dumpcoinstats() 1167 | +{ 1168 | + return RPCHelpMan{"dumpcoinstats", 1169 | + "\nDumps content of coinstats index to a CSV file.\n",
nit:
"Dumps content of coinstats index to a CSV file.",
Can remove some whitespace and \n.
done
28 | @@ -29,7 +29,7 @@ enum class CoinStatsHashType { 29 | 30 | struct CCoinsStats { 31 | //! Which hash type to use 32 | - const CoinStatsHashType m_hash_type; 33 | + CoinStatsHashType m_hash_type;
Wouldn't you have to delete the default constructor now? Otherwise it seems possible to leave this uninitialized?
Hm, ok, didn't think about that. I opted to set hash_serialized as a default value instead.
1181 | + HelpExampleCli("dumpcoinstats", "\"test\"") + 1182 | + HelpExampleRpc("dumpcoinstats", "\"test\"") 1183 | + }, 1184 | + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue 1185 | +{ 1186 | + if (request.params[0].isNull())
Missing { here and elsewhere?
fixed
Addressed comments by @Sjors and @MarcoFalke , thanks for reviewing!
1301 | + file << strprintf("%s,", FormatMoney(unspendables_unclaimed_rewards)); 1302 | + 1303 | + file << strprintf("%d,", pindex->nTx); 1304 | + file << strprintf("%d,", pindex->nTime); 1305 | + file << strprintf("%d,", pindex->GetMedianTimePast()); 1306 | + file << strprintf("%s,", pindex->nChainWork.GetHex());
3772c09e16544c7876da13f33e6cb11401c9e4bd : this is a cumulative metric. Would be nice to get the per block work.
done
1296 | @@ -1291,7 +1297,13 @@ static RPCHelpMan dumpcoinstats() 1297 | file << strprintf("%s,", FormatMoney(unspendables_scripts)); 1298 | 1299 | const CAmount unspendables_unclaimed_rewards = cumulative ? stats.total_unspendables_unclaimed_rewards : stats.total_unspendables_unclaimed_rewards - prev_stats.total_unspendables_unclaimed_rewards; 1300 | - file << strprintf("%s\n", FormatMoney(unspendables_unclaimed_rewards)); 1301 | + file << strprintf("%s,", FormatMoney(unspendables_unclaimed_rewards)); 1302 | + 1303 | + file << strprintf("%d,", pindex->nTx);
3772c09e16544c7876da13f33e6cb11401c9e4bd : this is not cumulative, so would be nice to have cumulative version.
done
tACK 3772c09e16544c7876da13f33e6cb11401c9e4bd
Remaining feedback can wait for a followup, but I'm also happy to review it again.
Maybe document the fields in the help, since some are not obvious. For each item, note if it has a cumulative version.
A slightly more logical ordering of columns would be:
txins to count the number of outputs spent. I think you can use coin_count between two blocks and txouts to calculate it.amount_spent)amount_created)This puts the most useful fields first.
There may be some metrics that are useful for both cumulative and non-cumulative. In particular it's both useful to know how many outputs were created and destroyed in any given block (and their value, and the delta UTXO set size), and how many outputs existed at any given block (and much value they represented and disk space they used).
1263 | + throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set"); 1264 | + 1265 | + file << strprintf("%d,", static_cast<int64_t>(stats.nHeight)); 1266 | + file << strprintf("%s,", stats.hashBlock.GetHex()); 1267 | + file << strprintf("%d,", static_cast<int64_t>(stats.nTransactionOutputs)); 1268 | + file << strprintf("%d,", static_cast<int64_t>(stats.nBogoSize));
The per-block delta in bogosize is useful too, for the non cumulative version.
done
Addressed @Sjors feedback and also squashed some commits because it was just easier to do the reorganization this way.
Maybe document the fields in the help, since some are not obvious. For each item, note if it has a cumulative version.
I was a bit unsure where to put this and how to format it but I tried my best.
A slightly more logical ordering of columns would be:
I applied that order
- maybe insert:
txinsto count the number of outputs spent. I think you can usecoin_countbetween two blocks andtxoutsto calculate it.
I think it's not possible without updating coinstatsindex. The coin_count isn't included there, I am not sure why but probably just bad timing since it was merged while I was already working on the index. Leaving this as a follow-up since I first have to think about updating the index. I also thought about calculating it with txouts but I think it would be (txouts - prev_txouts) - coinbase_txouts - unspendable_txouts and from those I am missing the unspendable counts as well.
- txouts (document for cumulative: this is the total number of outputs ever created, not the total in existence at this height)
I actually didn't have this as non-cummulative yet, but done as well.
- maybe insert: the number txouts in existence at this height (the same for cumulative). It may be useful to track its ratio to bogosize over time.
I think you mean UTXOs here which would basically be coin_count? Or should it also include unspendable outputs?
- prevout_spent_amount (maybe rename to
amount_spent)- amount (or
amount_created)
renamed both
- unspendable_amount (docs should clarify this includes both immature and OP_RETURN)
No, immature is not included here. This is the just the total of the other unspendable values (genesis, bip30, scripts and unclaimed rewards)
1292 | + file << strprintf("%s,", stats.hashBlock.GetHex()); 1293 | + file << strprintf("%d,", pindex->nTime); 1294 | + file << strprintf("%d,", pindex->GetMedianTimePast()); 1295 | + file << strprintf("%s,", pindex->nVersion); 1296 | + 1297 | + const int num_tx = cumulative ? pindex->nTx : pindex->nTx - (pindex->pprev ? pindex->pprev->nTx : 0);
nTx is not cumulative
num_tx_sum += pindex->nTx;
const int num_tx = cumulative ? num_tx_sum : pindex->nTx;
Done
1281 | + coins_view = &active_chainstate.CoinsDB(); 1282 | + blockman = &active_chainstate.m_blockman; 1283 | + } 1284 | + 1285 | + CCoinsStats prev_stats{CoinStatsHashType::MUHASH}; 1286 | +
uint64_t num_tx_sum = 0;
Done
1171 | + "- block_hash\n" 1172 | + "- timestamp\n" 1173 | + "- mediantime\n" 1174 | + "- version\n" 1175 | + "- (total_)txs - Number of transactions included\n" 1176 | + "- (total_)txouts - Number of transaction outputs created\n"
"Nett number of" or "outputs added"
See e.g. block 170 that has a coinbase output and a regular transaction with one input and two outputs, yet txouts is 2.
https://www.blockchain.com/btc/block/00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee
(it'd be nice to actually know how many were created, but see discussion elsewhere)
Done
1173 | + "- mediantime\n" 1174 | + "- version\n" 1175 | + "- (total_)txs - Number of transactions included\n" 1176 | + "- (total_)txouts - Number of transaction outputs created\n" 1177 | + "- (total_)amount_spent - Amount of inputs spent\n" 1178 | + "- (total_)amount_created - Amount of coins added to the UTXO set\n"
Maybe call this amount_generated and add "(never more than the block subsidy)
Done
1176 | + "- (total_)txouts - Number of transaction outputs created\n" 1177 | + "- (total_)amount_spent - Amount of inputs spent\n" 1178 | + "- (total_)amount_created - Amount of coins added to the UTXO set\n" 1179 | + "- (total_)coinbase_amount - Coinbase subsidy amount\n" 1180 | + "- (total_)new_outputs_ex_coinbase_amount - Amount of new outputs created by this block\n" 1181 | + "- (total_)subsidy - Total subsidy paid out\n"
Should be "Maximum subsidy"
Block 501,726 (0000000000000000004b27f9ee7ba33d6f048f684aaeb0eea4befd80f1701126) claimed 0 rewards, yet its subsidycolumn says 12.5.
Alternatively we can keep subsidy what it is, but then amount_generated should be negative.
https://bitcoin.stackexchange.com/questions/38994/will-there-be-21-million-bitcoins-eventually
Unrelated bug: the unspendable_amount for this block is set to 12.5 but should be zero (also wrong in gettxoutsetinfo). Same issue for block 526,591 which claimed half its reward (the other half is not unspendable, it doesn't exist). This is different than the situation with the genesis block: that block does exist and does claim a reward, but it just can't be spent.
Hehe, we are really getting into the weeds here :)
Yeah, the "paid out" part is definitely wrong. It wasn't paid out but also it can never be paid out in the the future, I still find this hard to express concisely so changed to "Maximum subsidy".
For the unspendable_amount comment: that is not a bug by my definition. When I wrote the coinstatsindex one of my goals was to make these non-existent amounts visible, and I summed this up with the other categories of lost coins under the umbrella term unspendables . See this code block in coinstatsindex:
const CAmount unclaimed_rewards{(m_total_prevout_spent_amount + m_total_subsidy) - (m_total_new_outputs_ex_coinbase_amount + m_total_coinbase_amount + m_total_unspendable_amount)};
m_total_unspendable_amount += unclaimed_rewards;
m_total_unspendables_unclaimed_rewards += unclaimed_rewards;
If the definition for anything "unspendable" was that it needed have to existed before as an output in a tx, then yes, the unclaimed rewards should not be part of this category. But I think this is confusing to some without deep knowledge of how the subsidies work. They might think that these amounts could still be claimed some time in the future, since they haven't existed yet and there will be 21M, right? So I was more looking at this from the side of the 21M: for example the 12.5 are part of the 21M total (and part of the >19M that are considered mined) even though they will never exist because the moment to claim them has passed. Thus, I consider them unspendable even though they never even existed in the first place, i.e. "unspendable" includes "never spendable in the future". If we don't keep track of these to some degree we can never really do the full accounting that this export was intended for.
I see, in that case just document that "unspendable" means "unspendable coins plus unclaimed subsidies".
Though I think it's cleaner to track unspendable and unclaimed_rewards separately, and then explain in the docs that "unclaimed" means it can never be claimed. I suspect anyone using an RPC like this will understand that.
Done, amended the doc for the unspendable_amount.
I see your reasoning but at least for now let's keep it consistent with the index code and gettxoutsetinfo where the unclaimed rewards are in the unspendable 'category'.
1174 | + "- version\n" 1175 | + "- (total_)txs - Number of transactions included\n" 1176 | + "- (total_)txouts - Number of transaction outputs created\n" 1177 | + "- (total_)amount_spent - Amount of inputs spent\n" 1178 | + "- (total_)amount_created - Amount of coins added to the UTXO set\n" 1179 | + "- (total_)coinbase_amount - Coinbase subsidy amount\n"
"Amount claimed in coinbase transaction." (see e.g. block 409,008 which claimed a whopping 17K BTC.
Done
87f0cc381c77e7d321d9e01a11a4725878c81810 almost there
Leaving this as a follow-up since I first have to think about updating the index
Yeah, let's not touch the index itself here.
I think you mean UTXOs here which would basically be
coin_count? Or should it also include unspendable outputs?
Excluding unspendable sounds good to me. Other than the coinbase, they're not really part of the UTXO set in practice, e.g. they don't use RAM and their bogo_size is zero too.
Seeing this in a spreadsheet slightly changed my thoughts on the optimal order. After txouts let's do:
(then continue as it is now, from unspendable_scripts)
Seeing this in a spreadsheet slightly changed my thoughts on the optimal order. After txouts let's do:
- coinbase_amount
- subsidy (which represents the maximum allowed subsidy)
- amount_generated (in theory this can be less than zero, but afaik there has never been a block that included transactions but forgot to claim both the subsidy and fees)
- unclaimed_rewards
- amount_spent
- new_outputs_ex_coinbase_amount
(then continue as it is now, from unspendable_scripts)
Done
tACK 418331f35424bdc95dedba6023bf0d8f7819fabe
TIL block 367,853 has the highest number of transactions ever: 12,239
Added a wish list for more stats: https://github.com/bitcoin/bitcoin/issues/24581
re-utACK 843159e5df3dc0d1ca0341b22aaa9fe86160994a
Rebased
Half of CI seems unhappy...
re-tACK 19b12fa07653cb11a6fd97566af0036241eaa8c9
836 | + HelpExampleCli("dumpcoinstats", "\"test\"") + 837 | + HelpExampleRpc("dumpcoinstats", "\"test\"") 838 | + }, 839 | + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue 840 | +{ 841 | + if (request.params[0].isNull()) {
nit: I think for new code it is fine to use the normal indent (8 spaces and 12 spaces for those lines, respectively)
done
Rebased and changed indentation per @MarcoFalke 's request.
919 | + CCoinsStats prev_stats{CoinStatsHashType::MUHASH}; 920 | + uint64_t num_tx_sum = 0; 921 | + 922 | + for (; pindex; pindex = active_chainstate.m_chain.Next(pindex)) { 923 | + if (!GetUTXOStats(coins_view, *blockman, stats, node.rpc_interruption_point, pindex)) 924 | + throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
nit: Perhaps the error message could be more specific?
throw JSONRPCError(RPC_INTERNAL_ERROR, "Failed to generate coins stats");
Thanks for the quick re-review! I adapted your suggestion by adding the height of the failure as well, which should make is more helpful in case of corruption in the index.
Rebased. The first commit has become obsolete, so it was removed.
re-tACK 3b3e3d966ad44b37bab3659b0d840f8384da05be
986 | @@ -985,6 +987,202 @@ static RPCHelpMan gettxoutsetinfo() 987 | }; 988 | } 989 | 990 | +static RPCHelpMan dumpcoinstats() 991 | +{ 992 | + return RPCHelpMan{"dumpcoinstats", 993 | + "Dumps content of coinstats index to a CSV file. The resulting file contains the following columns:\n"
Could mention that this will have a line for each block?
done
1039 | + throw JSONRPCError(RPC_INVALID_PARAMETER, filepath.u8string() + " already exists. If you are sure this is what you want, move it out of the way first"); 1040 | + } 1041 | + 1042 | + std::ofstream file; 1043 | + file.open(filepath); 1044 | + if (!file.is_open())
if (!file.is_open()) {
done
1092 | + } 1093 | + 1094 | + NodeContext& node = EnsureAnyNodeContext(request.context); 1095 | + ChainstateManager& chainman = EnsureChainman(node); 1096 | + CChainState& active_chainstate = chainman.ActiveChainstate(); 1097 | + active_chainstate.ForceFlushStateToDisk();
Pretty sure flushing and getting the cursor will need to happen in the same cs_main scope, otherwise the flush seems best-effort at best?
Yeah, I would have described it as best effort so I think it can be omitted here as well. And this made me realize that we don't actually need coins_view and blockman, so I adapted this as well.
1109 | + uint64_t num_tx_sum = 0; 1110 | + 1111 | + for (; pindex; pindex = active_chainstate.m_chain.Next(pindex)) { 1112 | + const std::optional<CCoinsStats> maybe_stats{GetUTXOStats(coins_view, *blockman, CoinStatsHashType::MUHASH, node.rpc_interruption_point, pindex)}; 1113 | + 1114 | + if (!maybe_stats)
if (!maybe_stats) {
Also, could mention that the file may have been created and may need to be manually cleared now?
done
1112 | + const std::optional<CCoinsStats> maybe_stats{GetUTXOStats(coins_view, *blockman, CoinStatsHashType::MUHASH, node.rpc_interruption_point, pindex)}; 1113 | + 1114 | + if (!maybe_stats) 1115 | + throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf("Failed to get coins stats at height %d", pindex->nHeight)); 1116 | + 1117 | + CCoinsStats stats{maybe_stats.value()};
Any reason to create a copy when a reference should be enough?
done
310 | + if cumulative: 311 | + file = "ctest.csv" 312 | + else: 313 | + file = "test.csv" 314 | + 315 | + path = os.path.join(get_datadir_path(self.options.tmpdir, 1), file)
Probably time to introduce a helper and use that:
diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py
index 7d2db391b6..aa9ff9831f 100755
--- a/test/functional/test_framework/test_node.py
+++ b/test/functional/test_framework/test_node.py
@@ -381,9 +381,13 @@ class TestNode():
def wait_until_stopped(self, timeout=BITCOIND_PROC_WAIT_TIMEOUT):
wait_until_helper(self.is_node_stopped, timeout=timeout, timeout_factor=self.timeout_factor)
+ [@property](/bitcoin-bitcoin/contributor/property/)
+ def datadir_path(self) -> Path:
+ return Path(self.datadir)
+ [@property](/bitcoin-bitcoin/contributor/property/)
def chain_path(self) -> Path:
- return Path(self.datadir) / self.chain
+ return self.datadir_path / self.chain [@property](/bitcoin-bitcoin/contributor/property/)
def debug_log_path(self) -> Path:
path = self.nodes[1].datadir_path / file
I like the helper but using the / operator would mean file would also need to be a Path object iiuc. So, instead I opted for a slightly different approach that I liked a little better using another helper datadir_file_path. What do you think?
but using the / operator would mean file would also need to be a Path object iiuc
Does it? (It doesn't for me locally)
Hm, seems like I misinterpreted the error, actually the next line fails. And I am now also seeing it fail with the Path object but I am sure it previously passed at least twice with it. Very strange. Maybe a python version or platform issue, need to look deeper into this.
2022-06-11T16:28:24.998000Z TestFramework (INFO): Test dumpcoinstats RPC
2022-06-11T16:28:24.998000Z TestFramework (ERROR): Unexpected exception caught during testing
Traceback (most recent call last):
File "/Users/FJ/projects/clones/bitcoin/test/functional/test_framework/test_framework.py", line 133, in main
self.run_test()
File "/Users/FJ/projects/clones/bitcoin/test/functional/feature_coinstatsindex.py", line 58, in run_test
self._test_dumpcoinstats_rpc()
File "/Users/FJ/projects/clones/bitcoin/test/functional/feature_coinstatsindex.py", line 319, in _test_dumpcoinstats_rpc
self.nodes[1].dumpcoinstats(path, cumulative)
File "/Users/FJ/projects/clones/bitcoin/test/functional/test_framework/coverage.py", line 49, in __call__
return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
File "/Users/FJ/projects/clones/bitcoin/test/functional/test_framework/authproxy.py", line 141, in __call__
postdata = json.dumps(self.get_request(*args, **argsn), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
File "/Users/FJ/projects/clones/bitcoin/test/functional/test_framework/authproxy.py", line 131, in get_request
json.dumps(args or argsn, default=EncodeDecimal, ensure_ascii=self.ensure_ascii),
File "/opt/homebrew/Cellar/python@3.9/3.9.12/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/__init__.py", line 234, in dumps
return cls(
File "/opt/homebrew/Cellar/python@3.9/3.9.12/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py", line 199, in encode
chunks = self.iterencode(o, _one_shot=True)
File "/opt/homebrew/Cellar/python@3.9/3.9.12/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py", line 257, in iterencode
return _iterencode(o, 0)
File "/Users/FJ/projects/clones/bitcoin/test/functional/test_framework/authproxy.py", line 66, in EncodeDecimal
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: PosixPath('/var/folders/9z/n7rz_6cj3bq__11k5kcrsvvm0000gn/T/bitcoin_func_test_ru27w0qw/node1/ctest.csv') is not JSON serializable
You could do str(path) before passing it as json?
Yeah, that works too and I just pushed a version with that. But I find it a little less elegant overall.
Not sure about the cs_main locking assumptions
111 | @@ -112,7 +112,7 @@ def run_test(self): 112 | self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo$bar$baz']) 113 | 114 | self.log.info('Check that failure to write cookie file will abort the node gracefully') 115 | - cookie_file = os.path.join(get_datadir_path(self.options.tmpdir, 0), self.chain, '.cookie.tmp') 116 | + cookie_file = str(self.nodes[0].datadir_path / self.chain / '.cookie.tmp')
cookie_file = self.nodes[0].chain_path / '.cookie.tmp'
nit?
nit!
If the index has not fully been synced yet, the error thrown would be:
Failed to get coins stats at height X. You may have to remove an incomplete dump file before running this command again.
It's not very clear that the index has not finished syncing. Also, the csv file would be created before this error is thrown, so the file would have to be removed manually.
I would suggest checking if the index is synced before dumping and create the file only if the index is ready+enabled, with something like this:
<details> <summary>Diff</summary>
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index 74877d7da..7bb6e2927 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -1039,16 +1039,21 @@ static RPCHelpMan dumpcoinstats()
throw JSONRPCError(RPC_INVALID_PARAMETER, filepath.u8string() + " already exists. If you are sure this is what you want, move it out of the way first");
}
+ if (!g_coin_stats_index) {
+ throw JSONRPCError(RPC_MISC_ERROR, "Coinstats index is not enabled");
+ }
+
+ const auto summary{g_coin_stats_index->GetSummary()};
+ if (!summary.synced) {
+ throw JSONRPCError(RPC_MISC_ERROR, strprintf("coinstatsindex is not synced. Current best block height: %d", summary.best_block_height));
+ }
+
std::ofstream file;
file.open(filepath);
if (!file.is_open()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open dump file");
}
- if (!g_coin_stats_index) {
- throw JSONRPCError(RPC_MISC_ERROR, "Coinstats index is not enabled");
- }
-
const bool cumulative{request.params[1].isNull() ? false : request.params[1].get_bool()};
file << strprintf("height,"
</details>
1166 | + const uint64_t bogo_size = cumulative ? stats.nBogoSize : stats.nBogoSize - prev_stats.nBogoSize; 1167 | + file << strprintf("%d\n", static_cast<int64_t>(bogo_size)); 1168 | + 1169 | + prev_stats = stats; 1170 | + 1171 | + if (pindex->nHeight >= active_chainstate.m_chain.Tip()->nHeight) break;
Is it possible that the index has not synced up to active_chainstate.m_chain.Tip() if the tip moved during the dump? I don't see any lock for that. Could be fixed with this?
if (pindex->nHeight >= g_coin_stats_index->GetSummary().best_block_height) break;
Yeah, I agree, there is probably a very small chance of this happening and it is avoided with this change.
Addressed comments from @aureleoules , thanks for the review!
Further note: I can't change the status back it seems, but please treat this as a draft for now. This PR will need to be updated and rebased soon when I have resolved the overflow issue noted in #26362.
EDIT: Rebased as well since there was a silent merge conflict.
This RPC dumps the full content of the coinstats index together with some other statistics into a CSV file.
<!--cf906140f33d8803c4a75a2196329ecb-->
🐙 This pull request conflicts with the target branch and needs rebase.
<!--13523179cfe9479db18ec6c5d236f789-->
There hasn't been much activity lately and the patch still needs rebase. What is the status here?
There doesn't seem to be much interest in this feature currently. If someone needs it, feel free to ping me here and I will reorg/rebase for you.