Skip to content

Commit 993cafe

Browse files
committed
RPC: Add type parameter to dumptxoutset
1 parent fccf4f9 commit 993cafe

File tree

5 files changed

+103
-7
lines changed

5 files changed

+103
-7
lines changed

src/rpc/blockchain.cpp

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2648,6 +2648,25 @@ static RPCHelpMan getblockfilter()
26482648
};
26492649
}
26502650

2651+
/**
2652+
* RAII class that disables the network in its constructor and enables it in its
2653+
* destructor.
2654+
*/
2655+
class NetworkDisable
2656+
{
2657+
CConnman& m_connman;
2658+
public:
2659+
NetworkDisable(CConnman& connman) : m_connman(connman) {
2660+
m_connman.SetNetworkActive(false);
2661+
if (m_connman.GetNetworkActive()) {
2662+
throw JSONRPCError(RPC_MISC_ERROR, "Network activity could not be suspended.");
2663+
}
2664+
};
2665+
~NetworkDisable() {
2666+
m_connman.SetNetworkActive(true);
2667+
};
2668+
};
2669+
26512670
/**
26522671
* Serialize the UTXO set to a file for loading elsewhere.
26532672
*
@@ -2660,6 +2679,14 @@ static RPCHelpMan dumptxoutset()
26602679
"Write the serialized UTXO set to a file.",
26612680
{
26622681
{"path", RPCArg::Type::STR, RPCArg::Optional::NO, "Path to the output file. If relative, will be prefixed by datadir."},
2682+
{"type", RPCArg::Type::STR, RPCArg::Default(""), "The type of snapshot to create. Can be \"latest\" to create a snapshot of the current UTXO set or \"rollback\" to temporarily roll back the state of the node to a historical block before creating the snapshot of a historical UTXO set. This parameter can be omitted if a separate \"rollback\" named parameter is specified indicating the height or hash of a specific historical block. If \"rollback\" is specified and separate \"rollback\" named parameter is not specified, this will roll back to the latest valid snapshot block that currently be loaded with loadtxoutset."},
2683+
{"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "",
2684+
{
2685+
{"rollback", RPCArg::Type::NUM, RPCArg::Optional::OMITTED,
2686+
"Height or hash of the block to roll back to before creating the snapshot. Note: The further this number is from the tip, the longer this process will take. Consider setting a higher -rpcclienttimeout value in this case.",
2687+
RPCArgOptions{.skip_type_check = true, .type_str = {"", "string or numeric"}}},
2688+
},
2689+
},
26632690
},
26642691
RPCResult{
26652692
RPCResult::Type::OBJ, "", "",
@@ -2673,10 +2700,33 @@ static RPCHelpMan dumptxoutset()
26732700
}
26742701
},
26752702
RPCExamples{
2676-
HelpExampleCli("dumptxoutset", "utxo.dat")
2703+
HelpExampleCli("-rpcclienttimeout=0 dumptxoutset", "utxo.dat latest") +
2704+
HelpExampleCli("-rpcclienttimeout=0 dumptxoutset", "utxo.dat rollback") +
2705+
HelpExampleCli("-rpcclienttimeout=0 -named dumptxoutset", R"(utxo.dat rollback=853456)")
26772706
},
26782707
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
26792708
{
2709+
NodeContext& node = EnsureAnyNodeContext(request.context);
2710+
const CBlockIndex* tip{WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Tip())};
2711+
const CBlockIndex* target_index{nullptr};
2712+
const std::string snapshot_type{self.Arg<std::string>("type")};
2713+
const UniValue options{request.params[2].isNull() ? UniValue::VOBJ : request.params[2]};
2714+
if (options.exists("rollback")) {
2715+
if (!snapshot_type.empty() && snapshot_type != "rollback") {
2716+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid snapshot type \"%s\" specified with rollback option", snapshot_type));
2717+
}
2718+
target_index = ParseHashOrHeight(options["rollback"], *node.chainman);
2719+
} else if (snapshot_type == "rollback") {
2720+
auto snapshot_heights = node.chainman->GetParams().GetAvailableSnapshotHeights();
2721+
CHECK_NONFATAL(snapshot_heights.size() > 0);
2722+
auto max_height = std::max_element(snapshot_heights.begin(), snapshot_heights.end());
2723+
target_index = ParseHashOrHeight(*max_height, *node.chainman);
2724+
} else if (snapshot_type == "latest") {
2725+
target_index = tip;
2726+
} else {
2727+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid snapshot type \"%s\" specified. Please specify \"rollback\" or \"latest\"", snapshot_type));
2728+
}
2729+
26802730
const ArgsManager& args{EnsureAnyArgsman(request.context)};
26812731
const fs::path path = fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(request.params[0].get_str()));
26822732
// Write to a temporary path and then move into `path` on completion
@@ -2698,11 +2748,55 @@ static RPCHelpMan dumptxoutset()
26982748
"Couldn't open file " + temppath.utf8string() + " for writing.");
26992749
}
27002750

2701-
NodeContext& node = EnsureAnyNodeContext(request.context);
2751+
CConnman& connman = EnsureConnman(node);
2752+
const CBlockIndex* invalidate_index{nullptr};
2753+
std::unique_ptr<NetworkDisable> disable_network;
2754+
2755+
// If the user wants to dump the txoutset of the current tip, we don't have
2756+
// to roll back at all
2757+
if (target_index != tip) {
2758+
// If the node is running in pruned mode we ensure all necessary block
2759+
// data is available before starting to roll back.
2760+
if (node.chainman->m_blockman.IsPruneMode()) {
2761+
LOCK(node.chainman->GetMutex());
2762+
const CBlockIndex* current_tip{node.chainman->ActiveChain().Tip()};
2763+
const CBlockIndex* first_block{node.chainman->m_blockman.GetFirstBlock(*current_tip, /*status_mask=*/BLOCK_HAVE_MASK)};
2764+
if (first_block->nHeight > target_index->nHeight) {
2765+
throw JSONRPCError(RPC_MISC_ERROR, "Could not roll back to requested height since necessary block data is already pruned.");
2766+
}
2767+
}
2768+
2769+
// Suspend network activity for the duration of the process when we are
2770+
// rolling back the chain to get a utxo set from a past height. We do
2771+
// this so we don't punish peers that send us that send us data that
2772+
// seems wrong in this temporary state. For example a normal new block
2773+
// would be classified as a block connecting an invalid block.
2774+
disable_network = std::make_unique<NetworkDisable>(connman);
2775+
2776+
invalidate_index = WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Next(target_index));
2777+
InvalidateBlock(*node.chainman, invalidate_index->GetBlockHash());
2778+
const CBlockIndex* new_tip_index{WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Tip())};
2779+
2780+
// In case there is any issue with a block being read from disk we need
2781+
// to stop here, otherwise the dump could still be created for the wrong
2782+
// height.
2783+
// The new tip could also not be the target block if we have a stale
2784+
// sister block of invalidate_index. This block (or a descendant) would
2785+
// be activated as the new tip and we would not get to new_tip_index.
2786+
if (new_tip_index != target_index) {
2787+
ReconsiderBlock(*node.chainman, invalidate_index->GetBlockHash());
2788+
throw JSONRPCError(RPC_MISC_ERROR, "Could not roll back to requested height, reverting to tip.");
2789+
}
2790+
}
2791+
27022792
UniValue result = CreateUTXOSnapshot(
27032793
node, node.chainman->ActiveChainstate(), afile, path, temppath);
27042794
fs::rename(temppath, path);
27052795

2796+
if (invalidate_index) {
2797+
ReconsiderBlock(*node.chainman, invalidate_index->GetBlockHash());
2798+
}
2799+
27062800
result.pushKV("path", path.utf8string());
27072801
return result;
27082802
},

src/rpc/client.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
187187
{ "gettxoutproof", 0, "txids" },
188188
{ "gettxoutsetinfo", 1, "hash_or_height" },
189189
{ "gettxoutsetinfo", 2, "use_index"},
190+
{ "dumptxoutset", 2, "options" },
191+
{ "dumptxoutset", 2, "rollback" },
190192
{ "lockunspent", 0, "unlock" },
191193
{ "lockunspent", 1, "transactions" },
192194
{ "lockunspent", 2, "persistent" },

test/functional/feature_assumeutxo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ def run_test(self):
295295
assert_equal(n1.getblockcount(), START_HEIGHT)
296296

297297
self.log.info(f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}")
298-
dump_output = n0.dumptxoutset('utxos.dat')
298+
dump_output = n0.dumptxoutset('utxos.dat', "latest")
299299

300300
self.log.info("Test loading snapshot when the node tip is on the same block as the snapshot")
301301
assert_equal(n0.getblockcount(), SNAPSHOT_BASE_HEIGHT)

test/functional/rpc_dumptxoutset.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def run_test(self):
2727
self.generate(node, COINBASE_MATURITY)
2828

2929
FILENAME = 'txoutset.dat'
30-
out = node.dumptxoutset(FILENAME)
30+
out = node.dumptxoutset(FILENAME, "latest")
3131
expected_path = node.datadir_path / self.chain / FILENAME
3232

3333
assert expected_path.is_file()
@@ -51,10 +51,10 @@ def run_test(self):
5151

5252
# Specifying a path to an existing or invalid file will fail.
5353
assert_raises_rpc_error(
54-
-8, '{} already exists'.format(FILENAME), node.dumptxoutset, FILENAME)
54+
-8, '{} already exists'.format(FILENAME), node.dumptxoutset, FILENAME, "latest")
5555
invalid_path = node.datadir_path / "invalid" / "path"
5656
assert_raises_rpc_error(
57-
-8, "Couldn't open file {}.incomplete for writing".format(invalid_path), node.dumptxoutset, invalid_path)
57+
-8, "Couldn't open file {}.incomplete for writing".format(invalid_path), node.dumptxoutset, invalid_path, "latest")
5858

5959

6060
if __name__ == '__main__':

test/functional/wallet_assumeutxo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def run_test(self):
9393

9494
self.log.info(
9595
f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}")
96-
dump_output = n0.dumptxoutset('utxos.dat')
96+
dump_output = n0.dumptxoutset('utxos.dat', "latest")
9797

9898
assert_equal(
9999
dump_output['txoutset_hash'],

0 commit comments

Comments
 (0)