Skip to content

Commit 4ee0e9e

Browse files
committed
rpc: Generate UTXO set hints
1 parent ed7b980 commit 4ee0e9e

File tree

3 files changed

+124
-0
lines changed

3 files changed

+124
-0
lines changed

src/rpc/blockchain.cpp

Lines changed: 122 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>
@@ -3396,6 +3397,126 @@ static RPCHelpMan loadtxoutset()
33963397
};
33973398
}
33983399

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