Skip to content

Commit df271eb

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. Currently, the hintfile is made in sequential order, but this may be done in parallel as a future improvement.
1 parent d19a04f commit df271eb

File tree

3 files changed

+127
-0
lines changed

3 files changed

+127
-0
lines changed

src/rpc/blockchain.cpp

Lines changed: 125 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>
@@ -3392,6 +3393,129 @@ static RPCHelpMan loadtxoutset()
33923393
};
33933394
}
33943395

3396+
static RPCHelpMan generatetxohints()
3397+
{
3398+
return RPCHelpMan{
3399+
"generatetxohints",
3400+
"Build a file of hints for the state of the UTXO set at a particular height.\n"
3401+
"The purpose of said hints is to allow clients performing initial block download\n"
3402+
"to omit unnecessary disk I/O and CPU usage.\n"
3403+
"The hint file is constructed by reading in blocks sequentially and determining what outputs\n"
3404+
"will remain in the UTXO set. Network activity will be suspended during this process, and the\n"
3405+
"hint file may take a few hours to build."
3406+
"Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
3407+
{
3408+
{"path",
3409+
RPCArg::Type::STR,
3410+
RPCArg::Optional::NO,
3411+
"Path to the hint file. If relative, will be prefixed by datadir."},
3412+
{"rollback",
3413+
RPCArg::Type::NUM,
3414+
RPCArg::Optional::OMITTED,
3415+
"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.",
3416+
RPCArgOptions{
3417+
.skip_type_check = true,
3418+
.type_str = {"", "string or numeric"},}},
3419+
},
3420+
RPCResult{
3421+
RPCResult::Type::OBJ, "", "",
3422+
{
3423+
{RPCResult::Type::NUM, "height", "The stopping height encoded by the hint file."},
3424+
{RPCResult::Type::STR, "path", "Absolute path where the file was written."},
3425+
{RPCResult::Type::STR, "duration", "Time taken to build the file."},
3426+
}
3427+
},
3428+
RPCExamples{
3429+
HelpExampleCli("--rpcclienttimeout=0 generatetxohints", "signet.hints 270000"),
3430+
},
3431+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
3432+
{
3433+
const auto start{SteadyClock::now()};
3434+
NodeContext& node{EnsureAnyNodeContext(request.context)};
3435+
if (node.chainman->m_blockman.IsPruneMode()) {
3436+
throw JSONRPCError(RPC_MISC_ERROR, "Creating a hint file in pruned mode is not possible.");
3437+
}
3438+
const ArgsManager& args{EnsureAnyArgsman(request.context)};
3439+
const fs::path path = fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(self.Arg<std::string_view>("path")));
3440+
const fs::path temppath = path + ".incomplete";
3441+
if (fs::exists(path)) {
3442+
throw JSONRPCError(
3443+
RPC_INVALID_PARAMETER,
3444+
path.utf8string() + " already exists. If you are sure this is what you want, "
3445+
"move it out of the way first");
3446+
}
3447+
FILE* file{fsbridge::fopen(temppath, "wb")};
3448+
AutoFile afile{file};
3449+
if (afile.IsNull()) {
3450+
throw JSONRPCError(
3451+
RPC_INVALID_PARAMETER,
3452+
"Couldn't open file " + temppath.utf8string() + " for writing.");
3453+
}
3454+
CConnman& connman{EnsureConnman(node)};
3455+
NetworkDisable disable_net{NetworkDisable(connman)};
3456+
std::optional<TemporaryRollback> rollback;
3457+
if (!request.params[1].isNull()) {
3458+
const CBlockIndex* invalidate_index = ParseHashOrHeight(request.params[1], *node.chainman);
3459+
invalidate_index = WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Next(invalidate_index));
3460+
rollback.emplace(*node.chainman, *invalidate_index);
3461+
}
3462+
node.rpc_interruption_point();
3463+
LOCK(node.chainman->GetMutex());
3464+
CChain& active_chain{node.chainman->ActiveChain()};
3465+
Chainstate& active_state{node.chainman->ActiveChainstate()};
3466+
active_state.ForceFlushStateToDisk();
3467+
const CBlockIndex* end_index{active_chain.Tip()};
3468+
const auto tip_height{end_index->nHeight};
3469+
LogDebug(BCLog::RPC, "Active chain best tip %d", tip_height);
3470+
swiftsync::HintsfileWriter writer{swiftsync::HintsfileWriter(afile, tip_height)};
3471+
CBlockIndex* curr{active_chain.Next(active_chain.Genesis())};
3472+
uint64_t total_outputs_written{0};
3473+
while (curr) {
3474+
auto height{curr->nHeight};
3475+
if (height % 10000 == 0) {
3476+
LogDebug(BCLog::RPC, "Wrote hints up to height %s, total outputs written %d, file size %d megabytes", height, total_outputs_written, writer.SizeMb());
3477+
}
3478+
FlatFilePos file_pos = curr->GetBlockPos();
3479+
std::unique_ptr<CBlock> pblock = std::make_unique<CBlock>();
3480+
bool read = node.chainman->m_blockman.ReadBlock(*pblock, file_pos, curr->GetBlockHash());
3481+
if (!read) {
3482+
throw JSONRPCError(RPC_DATABASE_ERROR, "Block could not be read from disk.");
3483+
}
3484+
swiftsync::BlockHintsWriter hints{};
3485+
for (const auto& tx: pblock->vtx) {
3486+
const Txid& txid = tx->GetHash();
3487+
for (size_t vout{}; vout < tx->vout.size(); ++vout) {
3488+
if (tx->vout[vout].scriptPubKey.IsUnspendable()) {
3489+
continue;
3490+
}
3491+
const COutPoint outpoint = COutPoint(txid, vout);
3492+
if (active_state.CoinsDB().HaveCoin(outpoint)) {
3493+
hints.PushHighBit();
3494+
} else {
3495+
hints.PushLowBit();
3496+
}
3497+
++total_outputs_written;
3498+
}
3499+
}
3500+
if (!writer.WriteNextUnspents(hints, uint32_t(height))) {
3501+
throw JSONRPCError(RPC_DATABASE_ERROR, "Failed to commit changes to hint file.");
3502+
}
3503+
node.rpc_interruption_point();
3504+
curr = active_chain.Next(curr);
3505+
}
3506+
writer.Close();
3507+
fs::rename(temppath, path);
3508+
const auto end{SteadyClock::now()};
3509+
const auto duration = std::chrono::duration_cast<std::chrono::minutes>(end - start).count();
3510+
UniValue result(UniValue::VOBJ);
3511+
result.pushKV("height", tip_height);
3512+
result.pushKV("path", path.utf8string());
3513+
result.pushKV("duration", tfm::format("%d minutes", duration));
3514+
return result;
3515+
},
3516+
};
3517+
}
3518+
33953519
const std::vector<RPCResult> RPCHelpForChainstate{
33963520
{RPCResult::Type::NUM, "blocks", "number of blocks in this chainstate"},
33973521
{RPCResult::Type::STR_HEX, "bestblockhash", "blockhash of the tip"},
@@ -3491,6 +3615,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
34913615
{"blockchain", &getblockfilter},
34923616
{"blockchain", &dumptxoutset},
34933617
{"blockchain", &loadtxoutset},
3618+
{"blockchain", &generatetxohints},
34943619
{"blockchain", &getchainstates},
34953620
{"hidden", &invalidateblock},
34963621
{"hidden", &reconsiderblock},

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
386386
{ "signmessagewithprivkey", 1, "message", ParamFormat::STRING },
387387
{ "walletpassphrasechange", 0, "oldpassphrase", ParamFormat::STRING },
388388
{ "walletpassphrasechange", 1, "newpassphrase", ParamFormat::STRING },
389+
{ "generatetxohints", 1, "rollback", ParamFormat::JSON_OR_STRING }
389390
};
390391
// clang-format on
391392

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)