mirror of
https://github.com/Retropex/bitcoin.git
synced 2025-06-02 07:22:33 +02:00
rpc: allow dumptxoutset to dump human-readable data
Co-authored-by: Shashwat Vangani <shaavan.github@gmail.com> Co-authored-by: Luke Dashjr <luke-jr+git@utopios.org>
This commit is contained in:
parent
1248d0da22
commit
c86d2d1cb3
@ -46,6 +46,7 @@
|
|||||||
#include <util/check.h>
|
#include <util/check.h>
|
||||||
#include <util/fs.h>
|
#include <util/fs.h>
|
||||||
#include <util/strencodings.h>
|
#include <util/strencodings.h>
|
||||||
|
#include <util/string.h>
|
||||||
#include <util/translation.h>
|
#include <util/translation.h>
|
||||||
#include <validation.h>
|
#include <validation.h>
|
||||||
#include <validationinterface.h>
|
#include <validationinterface.h>
|
||||||
@ -2647,11 +2648,32 @@ static RPCHelpMan getblockfilter()
|
|||||||
*/
|
*/
|
||||||
static RPCHelpMan dumptxoutset()
|
static RPCHelpMan dumptxoutset()
|
||||||
{
|
{
|
||||||
|
static const std::vector<std::pair<std::string, coinascii_cb_t>> ascii_types{
|
||||||
|
{"txid", [](const COutPoint& k, const Coin& c) { return k.hash.GetHex(); }},
|
||||||
|
{"vout", [](const COutPoint& k, const Coin& c) { return util::ToString(static_cast<int32_t>(k.n)); }},
|
||||||
|
{"value", [](const COutPoint& k, const Coin& c) { return util::ToString(c.out.nValue); }},
|
||||||
|
{"coinbase", [](const COutPoint& k, const Coin& c) { return util::ToString(c.fCoinBase); }},
|
||||||
|
{"height", [](const COutPoint& k, const Coin& c) { return util::ToString(static_cast<uint32_t>(c.nHeight)); }},
|
||||||
|
{"scriptPubKey", [](const COutPoint& k, const Coin& c) { return HexStr(c.out.scriptPubKey); }},
|
||||||
|
// add any other desired items here
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<RPCArg> ascii_args;
|
||||||
|
std::transform(std::begin(ascii_types), std::end(ascii_types), std::back_inserter(ascii_args),
|
||||||
|
[](const std::pair<std::string, coinascii_cb_t>& t) { return RPCArg{t.first, RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Info to write for a given UTXO"}; });
|
||||||
|
|
||||||
return RPCHelpMan{
|
return RPCHelpMan{
|
||||||
"dumptxoutset",
|
"dumptxoutset",
|
||||||
"Write the serialized UTXO set to a file.",
|
"Write the UTXO set to a file.",
|
||||||
{
|
{
|
||||||
{"path", RPCArg::Type::STR, RPCArg::Optional::NO, "Path to the output file. If relative, will be prefixed by datadir."},
|
{"path", RPCArg::Type::STR, RPCArg::Optional::NO, "Path to the output file. If relative, will be prefixed by datadir."},
|
||||||
|
{"format", RPCArg::Type::ARR, RPCArg::DefaultHint{"compact serialized format"},
|
||||||
|
"If no argument is provided, a compact binary serialized format is used; otherwise only requested items "
|
||||||
|
"available below are written in ASCII format (if an empty array is provided, all items are written in ASCII).",
|
||||||
|
ascii_args,
|
||||||
|
RPCArgOptions{.oneline_description="format"}},
|
||||||
|
{"show_header", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to include the header line in non-serialized (ASCII) mode"},
|
||||||
|
{"separator", RPCArg::Type::STR, RPCArg::Default{","}, "Field separator to use in non-serialized (ASCII) mode"},
|
||||||
},
|
},
|
||||||
RPCResult{
|
RPCResult{
|
||||||
RPCResult::Type::OBJ, "", "",
|
RPCResult::Type::OBJ, "", "",
|
||||||
@ -2665,10 +2687,33 @@ static RPCHelpMan dumptxoutset()
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
RPCExamples{
|
RPCExamples{
|
||||||
HelpExampleCli("dumptxoutset", "utxo.dat")
|
HelpExampleCli("dumptxoutset", "utxo.dat") +
|
||||||
|
HelpExampleCli("dumptxoutset", "utxo.dat '[]'") +
|
||||||
|
HelpExampleCli("dumptxoutset", "utxo.dat '[\"txid\", \"vout\"]' false ':'")
|
||||||
},
|
},
|
||||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||||
{
|
{
|
||||||
|
// handle optional ASCII parameters
|
||||||
|
const bool is_human_readable = !request.params[1].isNull();
|
||||||
|
const bool show_header = request.params[2].isNull() || request.params[2].get_bool();
|
||||||
|
const auto separator = request.params[3].isNull() ? MakeByteSpan(",").first(1) : MakeByteSpan(request.params[3].get_str());
|
||||||
|
std::vector<std::pair<std::string, coinascii_cb_t>> requested;
|
||||||
|
if (is_human_readable) {
|
||||||
|
const auto& arr = request.params[1].get_array();
|
||||||
|
const std::unordered_map<std::string, coinascii_cb_t> ascii_map(std::begin(ascii_types), std::end(ascii_types));
|
||||||
|
for (size_t i = 0; i < arr.size(); ++i) {
|
||||||
|
const auto it = ascii_map.find(arr[i].get_str());
|
||||||
|
if (it == std::end(ascii_map))
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "unable to find item '"+arr[i].get_str()+"'");
|
||||||
|
|
||||||
|
requested.emplace_back(*it);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if nothing was found, shows everything by default
|
||||||
|
if (requested.size() == 0)
|
||||||
|
requested = ascii_types;
|
||||||
|
}
|
||||||
|
|
||||||
const ArgsManager& args{EnsureAnyArgsman(request.context)};
|
const ArgsManager& args{EnsureAnyArgsman(request.context)};
|
||||||
const fs::path path = fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(request.params[0].get_str()));
|
const fs::path path = fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(request.params[0].get_str()));
|
||||||
// Write to a temporary path and then move into `path` on completion
|
// Write to a temporary path and then move into `path` on completion
|
||||||
@ -2682,7 +2727,7 @@ static RPCHelpMan dumptxoutset()
|
|||||||
"move it out of the way first");
|
"move it out of the way first");
|
||||||
}
|
}
|
||||||
|
|
||||||
FILE* file{fsbridge::fopen(temppath, "wb")};
|
FILE* file{fsbridge::fopen(temppath, !is_human_readable ? "wb" : "w")};
|
||||||
AutoFile afile{file};
|
AutoFile afile{file};
|
||||||
if (afile.IsNull()) {
|
if (afile.IsNull()) {
|
||||||
throw JSONRPCError(
|
throw JSONRPCError(
|
||||||
@ -2692,6 +2737,8 @@ static RPCHelpMan dumptxoutset()
|
|||||||
|
|
||||||
NodeContext& node = EnsureAnyNodeContext(request.context);
|
NodeContext& node = EnsureAnyNodeContext(request.context);
|
||||||
UniValue result = CreateUTXOSnapshot(
|
UniValue result = CreateUTXOSnapshot(
|
||||||
|
is_human_readable,
|
||||||
|
show_header, separator, requested,
|
||||||
node, node.chainman->ActiveChainstate(), afile, path, temppath);
|
node, node.chainman->ActiveChainstate(), afile, path, temppath);
|
||||||
fs::rename(temppath, path);
|
fs::rename(temppath, path);
|
||||||
|
|
||||||
@ -2702,6 +2749,10 @@ static RPCHelpMan dumptxoutset()
|
|||||||
}
|
}
|
||||||
|
|
||||||
UniValue CreateUTXOSnapshot(
|
UniValue CreateUTXOSnapshot(
|
||||||
|
const bool is_human_readable,
|
||||||
|
const bool show_header,
|
||||||
|
const Span<const std::byte>& separator,
|
||||||
|
const std::vector<std::pair<std::string, coinascii_cb_t>>& requested,
|
||||||
NodeContext& node,
|
NodeContext& node,
|
||||||
Chainstate& chainstate,
|
Chainstate& chainstate,
|
||||||
AutoFile& afile,
|
AutoFile& afile,
|
||||||
@ -2712,6 +2763,9 @@ UniValue CreateUTXOSnapshot(
|
|||||||
std::optional<CCoinsStats> maybe_stats;
|
std::optional<CCoinsStats> maybe_stats;
|
||||||
const CBlockIndex* tip;
|
const CBlockIndex* tip;
|
||||||
|
|
||||||
|
// used when human readable format is requested
|
||||||
|
const auto line_separator = MakeByteSpan("\n").first(1);
|
||||||
|
|
||||||
{
|
{
|
||||||
// We need to lock cs_main to ensure that the coinsdb isn't written to
|
// We need to lock cs_main to ensure that the coinsdb isn't written to
|
||||||
// between (i) flushing coins cache to disk (coinsdb), (ii) getting stats
|
// between (i) flushing coins cache to disk (coinsdb), (ii) getting stats
|
||||||
@ -2742,9 +2796,20 @@ UniValue CreateUTXOSnapshot(
|
|||||||
tip->nHeight, tip->GetBlockHash().ToString(),
|
tip->nHeight, tip->GetBlockHash().ToString(),
|
||||||
fs::PathToString(path), fs::PathToString(temppath)));
|
fs::PathToString(path), fs::PathToString(temppath)));
|
||||||
|
|
||||||
SnapshotMetadata metadata{chainstate.m_chainman.GetParams().MessageStart(), tip->GetBlockHash(), maybe_stats->coins_count};
|
if (!is_human_readable) {
|
||||||
|
SnapshotMetadata metadata{chainstate.m_chainman.GetParams().MessageStart(), tip->GetBlockHash(), maybe_stats->coins_count};
|
||||||
|
|
||||||
afile << metadata;
|
afile << metadata;
|
||||||
|
} else if (show_header) {
|
||||||
|
afile.write(MakeByteSpan("#(blockhash " + tip->GetBlockHash().ToString() + " ) "));
|
||||||
|
for (auto it = std::begin(requested); it != std::end(requested); ++it) {
|
||||||
|
if (it != std::begin(requested)) {
|
||||||
|
afile.write(separator);
|
||||||
|
}
|
||||||
|
afile.write(MakeByteSpan(it->first));
|
||||||
|
}
|
||||||
|
afile.write(line_separator);
|
||||||
|
}
|
||||||
|
|
||||||
COutPoint key;
|
COutPoint key;
|
||||||
Txid last_hash;
|
Txid last_hash;
|
||||||
@ -2776,12 +2841,22 @@ UniValue CreateUTXOSnapshot(
|
|||||||
if (iter % 5000 == 0) node.rpc_interruption_point();
|
if (iter % 5000 == 0) node.rpc_interruption_point();
|
||||||
++iter;
|
++iter;
|
||||||
if (pcursor->GetKey(key) && pcursor->GetValue(coin)) {
|
if (pcursor->GetKey(key) && pcursor->GetValue(coin)) {
|
||||||
if (key.hash != last_hash) {
|
if (!is_human_readable) {
|
||||||
write_coins_to_file(afile, last_hash, coins, written_coins_count);
|
if (key.hash != last_hash) {
|
||||||
last_hash = key.hash;
|
write_coins_to_file(afile, last_hash, coins, written_coins_count);
|
||||||
coins.clear();
|
last_hash = key.hash;
|
||||||
|
coins.clear();
|
||||||
|
}
|
||||||
|
coins.emplace_back(key.n, coin);
|
||||||
|
} else {
|
||||||
|
for (auto it = std::begin(requested); it != std::end(requested); ++it) {
|
||||||
|
if (it != std::begin(requested))
|
||||||
|
afile.write(separator);
|
||||||
|
afile.write(MakeByteSpan(it->second(key, coin)));
|
||||||
|
}
|
||||||
|
afile.write(line_separator);
|
||||||
|
++written_coins_count;
|
||||||
}
|
}
|
||||||
coins.emplace_back(key.n, coin);
|
|
||||||
}
|
}
|
||||||
pcursor->Next();
|
pcursor->Next();
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,10 @@
|
|||||||
#ifndef BITCOIN_RPC_BLOCKCHAIN_H
|
#ifndef BITCOIN_RPC_BLOCKCHAIN_H
|
||||||
#define BITCOIN_RPC_BLOCKCHAIN_H
|
#define BITCOIN_RPC_BLOCKCHAIN_H
|
||||||
|
|
||||||
|
#include <coins.h>
|
||||||
#include <consensus/amount.h>
|
#include <consensus/amount.h>
|
||||||
#include <core_io.h>
|
#include <core_io.h>
|
||||||
|
#include <span.h>
|
||||||
#include <streams.h>
|
#include <streams.h>
|
||||||
#include <sync.h>
|
#include <sync.h>
|
||||||
#include <util/fs.h>
|
#include <util/fs.h>
|
||||||
@ -26,6 +28,7 @@ struct NodeContext;
|
|||||||
} // namespace node
|
} // namespace node
|
||||||
|
|
||||||
static constexpr int NUM_GETBLOCKSTATS_PERCENTILES = 5;
|
static constexpr int NUM_GETBLOCKSTATS_PERCENTILES = 5;
|
||||||
|
using coinascii_cb_t = std::function<std::string(const COutPoint&, const Coin&)>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the difficulty of the net wrt to the given block index.
|
* Get the difficulty of the net wrt to the given block index.
|
||||||
@ -52,6 +55,10 @@ void CalculatePercentilesByWeight(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES],
|
|||||||
* @return a UniValue map containing metadata about the snapshot.
|
* @return a UniValue map containing metadata about the snapshot.
|
||||||
*/
|
*/
|
||||||
UniValue CreateUTXOSnapshot(
|
UniValue CreateUTXOSnapshot(
|
||||||
|
const bool is_human_readable,
|
||||||
|
const bool show_header,
|
||||||
|
const Span<const std::byte>& separator,
|
||||||
|
const std::vector<std::pair<std::string, coinascii_cb_t>>& requested,
|
||||||
node::NodeContext& node,
|
node::NodeContext& node,
|
||||||
Chainstate& chainstate,
|
Chainstate& chainstate,
|
||||||
AutoFile& afile,
|
AutoFile& afile,
|
||||||
|
@ -93,6 +93,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||||||
{ "scanblocks", 5, "options" },
|
{ "scanblocks", 5, "options" },
|
||||||
{ "scanblocks", 5, "filter_false_positives" },
|
{ "scanblocks", 5, "filter_false_positives" },
|
||||||
{ "scantxoutset", 1, "scanobjects" },
|
{ "scantxoutset", 1, "scanobjects" },
|
||||||
|
{ "dumptxoutset", 1, "format" },
|
||||||
|
{ "dumptxoutset", 2, "show_header" },
|
||||||
{ "addmultisigaddress", 0, "nrequired" },
|
{ "addmultisigaddress", 0, "nrequired" },
|
||||||
{ "addmultisigaddress", 1, "keys" },
|
{ "addmultisigaddress", 1, "keys" },
|
||||||
{ "createmultisig", 0, "nrequired" },
|
{ "createmultisig", 0, "nrequired" },
|
||||||
|
@ -48,6 +48,7 @@ CreateAndActivateUTXOSnapshot(
|
|||||||
AutoFile auto_outfile{outfile};
|
AutoFile auto_outfile{outfile};
|
||||||
|
|
||||||
UniValue result = CreateUTXOSnapshot(
|
UniValue result = CreateUTXOSnapshot(
|
||||||
|
false, false, Span<std::byte>(), {},
|
||||||
node, node.chainman->ActiveChainstate(), auto_outfile, snapshot_path, snapshot_path);
|
node, node.chainman->ActiveChainstate(), auto_outfile, snapshot_path, snapshot_path);
|
||||||
LogPrintf(
|
LogPrintf(
|
||||||
"Wrote UTXO snapshot to %s: %s\n", fs::PathToString(snapshot_path.make_preferred()), result.write());
|
"Wrote UTXO snapshot to %s: %s\n", fs::PathToString(snapshot_path.make_preferred()), result.write());
|
||||||
|
Loading…
Reference in New Issue
Block a user