Merge #16899: UTXO snapshot creation (dumptxoutset)

92b2f5306b test: add dumptxoutset RPC test (James O'Beirne)
c1ccbc3dde devtools: add utxo_snapshot.sh (James O'Beirne)
57cf74c991 rpc: add dumptxoutset (James O'Beirne)
92fafb3a7d coinstats: add coins_count (James O'Beirne)
707fde7b9b add unused SnapshotMetadata class (James O'Beirne)

Pull request description:

  This is part of the [assumeutxo project](https://github.com/bitcoin/bitcoin/projects/11):

  Parent PR: #15606
  Issue: #15605
  Specification: https://github.com/jamesob/assumeutxo-docs/tree/master/proposal

  ---

  This changeset defines the serialization format for UTXO snapshots and adds an RPC command for creating them, `dumptxoutset`. It also adds a convenience script for generating and verifying snapshots at a certain height, since that requires doing a hacky rewind of the chain via `invalidateblock`.

  All of this is unused at the moment.

ACKs for top commit:
  laanwj:
    ACK 92b2f5306b

Tree-SHA512: 200dff87767f157d627e99506ec543465d9329860a6cd49363081619c437163a640a46d008faa92b1f44fd403bfc7a7c9e851c658b5a4849efa9a34ca976bf31
This commit is contained in:
Wladimir J. van der Laan
2019-11-05 19:38:45 +01:00
9 changed files with 269 additions and 9 deletions

View File

@@ -15,6 +15,7 @@
#include <hash.h>
#include <index/blockfilterindex.h>
#include <node/coinstats.h>
#include <node/utxo_snapshot.h>
#include <policy/feerate.h>
#include <policy/policy.h>
#include <policy/rbf.h>
@@ -2245,6 +2246,113 @@ static UniValue getblockfilter(const JSONRPCRequest& request)
return ret;
}
/**
* Serialize the UTXO set to a file for loading elsewhere.
*
* @see SnapshotMetadata
*/
UniValue dumptxoutset(const JSONRPCRequest& request)
{
RPCHelpMan{
"dumptxoutset",
"\nWrite the serialized UTXO set to disk.\n"
"Incidentally flushes the latest coinsdb (leveldb) to disk.\n",
{
{"path",
RPCArg::Type::STR,
RPCArg::Optional::NO,
/* default_val */ "",
"path to the output file. If relative, will be prefixed by datadir."},
},
RPCResult{
"{\n"
" \"coins_written\": n, (numeric) the number of coins written in the snapshot\n"
" \"base_hash\": \"...\", (string) the hash of the base of the snapshot\n"
" \"base_height\": n, (string) the height of the base of the snapshot\n"
" \"path\": \"...\" (string) the absolute path that the snapshot was written to\n"
"]\n"
},
RPCExamples{
HelpExampleCli("dumptxoutset", "utxo.dat")
}
}.Check(request);
fs::path path = fs::absolute(request.params[0].get_str(), GetDataDir());
// Write to a temporary path and then move into `path` on completion
// to avoid confusion due to an interruption.
fs::path temppath = fs::absolute(request.params[0].get_str() + ".incomplete", GetDataDir());
if (fs::exists(path)) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
path.string() + " already exists. If you are sure this is what you want, "
"move it out of the way first");
}
FILE* file{fsbridge::fopen(temppath, "wb")};
CAutoFile afile{file, SER_DISK, CLIENT_VERSION};
std::unique_ptr<CCoinsViewCursor> pcursor;
CCoinsStats stats;
CBlockIndex* tip;
{
// 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
// based upon the coinsdb, and (iii) constructing a cursor to the
// coinsdb for use below this block.
//
// Cursors returned by leveldb iterate over snapshots, so the contents
// of the pcursor will not be affected by simultaneous writes during
// use below this block.
//
// See discussion here:
// https://github.com/bitcoin/bitcoin/pull/15606#discussion_r274479369
//
LOCK(::cs_main);
::ChainstateActive().ForceFlushStateToDisk();
if (!GetUTXOStats(&::ChainstateActive().CoinsDB(), stats)) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
}
pcursor = std::unique_ptr<CCoinsViewCursor>(::ChainstateActive().CoinsDB().Cursor());
tip = LookupBlockIndex(stats.hashBlock);
CHECK_NONFATAL(tip);
}
SnapshotMetadata metadata{tip->GetBlockHash(), stats.coins_count, tip->nChainTx};
afile << metadata;
COutPoint key;
Coin coin;
unsigned int iter{0};
while (pcursor->Valid()) {
if (iter % 5000 == 0 && !IsRPCRunning()) {
throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "Shutting down");
}
++iter;
if (pcursor->GetKey(key) && pcursor->GetValue(coin)) {
afile << key;
afile << coin;
}
pcursor->Next();
}
afile.fclose();
fs::rename(temppath, path);
UniValue result(UniValue::VOBJ);
result.pushKV("coins_written", stats.coins_count);
result.pushKV("base_hash", tip->GetBlockHash().ToString());
result.pushKV("base_height", tip->nHeight);
result.pushKV("path", path.string());
return result;
}
// clang-format off
static const CRPCCommand commands[] =
{ // category name actor (function) argNames
@@ -2281,6 +2389,7 @@ static const CRPCCommand commands[] =
{ "hidden", "waitforblock", &waitforblock, {"blockhash","timeout"} },
{ "hidden", "waitforblockheight", &waitforblockheight, {"height","timeout"} },
{ "hidden", "syncwithvalidationinterfacequeue", &syncwithvalidationinterfacequeue, {} },
{ "hidden", "dumptxoutset", &dumptxoutset, {"path"} },
};
// clang-format on