Skip to content

Commit 749c6ff

Browse files
committed
rpc: Generate UTXO set hints
Generate hints for the location of UTXOs in each block. This RPC allows for a rollback of the chainstate to an arbitary height. Outputs not included in the index of the hintsfile are: 1. provably unspendable outputs 2. BIP30 unspendable outputs 3. outputs spent within the same block
1 parent 0196a0f commit 749c6ff

File tree

3 files changed

+145
-0
lines changed

3 files changed

+145
-0
lines changed

src/rpc/blockchain.cpp

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include <script/descriptor.h>
4040
#include <serialize.h>
4141
#include <streams.h>
42+
#include <swiftsync.h>
4243
#include <sync.h>
4344
#include <tinyformat.h>
4445
#include <txdb.h>
@@ -3421,6 +3422,147 @@ static RPCHelpMan loadtxoutset()
34213422
};
34223423
}
34233424

3425+
static RPCHelpMan generatetxohints()
3426+
{
3427+
return RPCHelpMan{
3428+
"generatetxohints",
3429+
"Build a file of hints for the state of the UTXO set at a particular height.\n"
3430+
"The purpose of said hints is to allow clients performing initial block download\n"
3431+
"to omit unnecessary disk I/O and CPU usage.\n"
3432+
"The hint file is constructed by reading in blocks sequentially and determining what outputs\n"
3433+
"will remain in the UTXO set. Network activity will be suspended during this process, and the\n"
3434+
"hint file may take a few hours to build."
3435+
"Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
3436+
{
3437+
{"path",
3438+
RPCArg::Type::STR,
3439+
RPCArg::Optional::NO,
3440+
"Path to the hint file. If relative, will be prefixed by datadir."},
3441+
{"rollback",
3442+
RPCArg::Type::NUM,
3443+
RPCArg::Optional::OMITTED,
3444+
"The block hash or height to build the hint file up to. If none is provided, the file will be built from the current block tip.",
3445+
RPCArgOptions{
3446+
.skip_type_check = true,
3447+
.type_str = {"", "string or numeric"},}},
3448+
},
3449+
RPCResult{
3450+
RPCResult::Type::OBJ, "", "",
3451+
{
3452+
{RPCResult::Type::NUM, "height", "The stopping height encoded by the hint file."},
3453+
{RPCResult::Type::STR, "path", "Absolute path where the file was written."},
3454+
{RPCResult::Type::STR, "duration", "Time taken to build the file."},
3455+
}
3456+
},
3457+
RPCExamples{
3458+
HelpExampleCli("--rpcclienttimeout=0 generatetxohints", "signet.hints 270000"),
3459+
},
3460+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
3461+
{
3462+
const auto start{SteadyClock::now()};
3463+
NodeContext& node{EnsureAnyNodeContext(request.context)};
3464+
if (node.chainman->m_blockman.IsPruneMode()) {
3465+
throw JSONRPCError(RPC_MISC_ERROR, "Creating a hint file in pruned mode is not possible.");
3466+
}
3467+
const ArgsManager& args{EnsureAnyArgsman(request.context)};
3468+
const fs::path path = fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(self.Arg<std::string_view>("path")));
3469+
const fs::path temppath = path + ".incomplete";
3470+
if (fs::exists(path)) {
3471+
throw JSONRPCError(
3472+
RPC_INVALID_PARAMETER,
3473+
path.utf8string() + " already exists. If you are sure this is what you want, "
3474+
"move it out of the way first");
3475+
}
3476+
FILE* file{fsbridge::fopen(temppath, "wb")};
3477+
AutoFile afile{file};
3478+
if (afile.IsNull()) {
3479+
throw JSONRPCError(
3480+
RPC_INVALID_PARAMETER,
3481+
"Couldn't open file " + temppath.utf8string() + " for writing.");
3482+
}
3483+
CConnman& connman{EnsureConnman(node)};
3484+
NetworkDisable disable_net{NetworkDisable(connman)};
3485+
std::optional<TemporaryRollback> rollback;
3486+
if (!request.params[1].isNull()) {
3487+
const CBlockIndex* invalidate_index = ParseHashOrHeight(request.params[1], *node.chainman);
3488+
invalidate_index = WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Next(invalidate_index));
3489+
rollback.emplace(*node.chainman, *invalidate_index);
3490+
}
3491+
node.rpc_interruption_point();
3492+
LOCK(node.chainman->GetMutex());
3493+
CChain& active_chain{node.chainman->ActiveChain()};
3494+
Chainstate& active_state{node.chainman->ActiveChainstate()};
3495+
active_state.ForceFlushStateToDisk();
3496+
const CBlockIndex* end_index{active_chain.Tip()};
3497+
const auto tip_height{end_index->nHeight};
3498+
LogDebug(BCLog::RPC, "Active chain best tip %d", tip_height);
3499+
swiftsync::HintsfileWriter writer{swiftsync::HintsfileWriter(afile)};
3500+
if (!writer.WriteStopHeight(tip_height)) {
3501+
throw JSONRPCError(RPC_DATABASE_ERROR, "Failed to commit changes to hint file.");
3502+
}
3503+
CBlockIndex* curr{active_chain.Next(active_chain.Genesis())};
3504+
uint64_t total_outputs_written{0};
3505+
while (curr) {
3506+
auto height{curr->nHeight};
3507+
if (height % 10000 == 0) {
3508+
LogDebug(BCLog::RPC, "Wrote hints up to height %s, total outputs written %d", height, total_outputs_written);
3509+
}
3510+
FlatFilePos file_pos = curr->GetBlockPos();
3511+
std::unique_ptr<CBlock> pblock = std::make_unique<CBlock>();
3512+
bool read = node.chainman->m_blockman.ReadBlock(*pblock, file_pos, curr->GetBlockHash());
3513+
if (!read) {
3514+
throw JSONRPCError(RPC_DATABASE_ERROR, "Block could not be read from disk.");
3515+
}
3516+
std::set<COutPoint> spent_outputs{};
3517+
for (const auto& transaction : pblock->vtx) {
3518+
if (transaction->IsCoinBase()) {
3519+
continue;
3520+
}
3521+
for (const auto& txin : transaction->vin) {
3522+
spent_outputs.insert(txin.prevout);
3523+
}
3524+
}
3525+
uint32_t output_index{};
3526+
std::vector<uint32_t> unspent;
3527+
for (const auto& tx: pblock->vtx) {
3528+
if (tx->IsCoinBase() && IsBIP30Unspendable(curr->GetBlockHash(), curr->nHeight)) {
3529+
continue;
3530+
};
3531+
const Txid& txid = tx->GetHash();
3532+
for (size_t vout{}; vout < tx->vout.size(); ++vout) {
3533+
if (tx->vout[vout].scriptPubKey.IsUnspendable()) {
3534+
continue;
3535+
}
3536+
const COutPoint outpoint = COutPoint(txid, vout);
3537+
if (spent_outputs.contains(outpoint)) {
3538+
continue;
3539+
}
3540+
if (active_state.CoinsDB().HaveCoin(outpoint)) {
3541+
unspent.push_back(output_index);
3542+
}
3543+
++output_index;
3544+
++total_outputs_written;
3545+
}
3546+
}
3547+
auto ef{swiftsync::EliasFano::Compress(unspent)};
3548+
if (!writer.WriteHints(ef)) {
3549+
throw JSONRPCError(RPC_DATABASE_ERROR, "Failed to commit changes to hint file.");
3550+
}
3551+
node.rpc_interruption_point();
3552+
curr = active_chain.Next(curr);
3553+
}
3554+
fs::rename(temppath, path);
3555+
const auto end{SteadyClock::now()};
3556+
const auto duration = std::chrono::duration_cast<std::chrono::minutes>(end - start).count();
3557+
UniValue result(UniValue::VOBJ);
3558+
result.pushKV("height", tip_height);
3559+
result.pushKV("path", path.utf8string());
3560+
result.pushKV("duration", tfm::format("%d minutes", duration));
3561+
return result;
3562+
},
3563+
};
3564+
}
3565+
34243566
const std::vector<RPCResult> RPCHelpForChainstate{
34253567
{RPCResult::Type::NUM, "blocks", "number of blocks in this chainstate"},
34263568
{RPCResult::Type::STR_HEX, "bestblockhash", "blockhash of the tip"},
@@ -3520,6 +3662,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
35203662
{"blockchain", &getblockfilter},
35213663
{"blockchain", &dumptxoutset},
35223664
{"blockchain", &loadtxoutset},
3665+
{"blockchain", &generatetxohints},
35233666
{"blockchain", &getchainstates},
35243667
{"hidden", &invalidateblock},
35253668
{"hidden", &reconsiderblock},

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
388388
{ "signmessagewithprivkey", 1, "message", ParamFormat::STRING },
389389
{ "walletpassphrasechange", 0, "oldpassphrase", ParamFormat::STRING },
390390
{ "walletpassphrasechange", 1, "newpassphrase", ParamFormat::STRING },
391+
{ "generatetxohints", 1, "rollback", ParamFormat::JSON_OR_STRING }
391392
};
392393
// clang-format on
393394

src/test/fuzz/rpc.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const std::vector<std::string> RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{
7979
"echoipc", // avoid assertion failure (Assertion `"EnsureAnyNodeContext(request.context).init" && check' failed.)
8080
"generatetoaddress", // avoid prohibitively slow execution (when `num_blocks` is large)
8181
"generatetodescriptor", // avoid prohibitively slow execution (when `nblocks` is large)
82+
"generatetxohints", // avoid writing to disk
8283
"gettxoutproof", // avoid prohibitively slow execution
8384
"importmempool", // avoid reading from disk
8485
"loadtxoutset", // avoid reading from disk

0 commit comments

Comments
 (0)