Skip to content

Commit 94b0adc

Browse files
fjahrryanofsky
andcommitted
rpc, refactor: Prevent potential race conditions in dumptxoutset
Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
1 parent e868a6e commit 94b0adc

File tree

3 files changed

+76
-21
lines changed

3 files changed

+76
-21
lines changed

src/rpc/blockchain.cpp

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,22 @@ static GlobalMutex cs_blockchange;
7575
static std::condition_variable cond_blockchange;
7676
static CUpdatedBlock latestblock GUARDED_BY(cs_blockchange);
7777

78+
std::tuple<std::unique_ptr<CCoinsViewCursor>, CCoinsStats, const CBlockIndex*>
79+
PrepareUTXOSnapshot(
80+
Chainstate& chainstate,
81+
const std::function<void()>& interruption_point = {})
82+
EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
83+
84+
UniValue WriteUTXOSnapshot(
85+
Chainstate& chainstate,
86+
CCoinsViewCursor* pcursor,
87+
CCoinsStats* maybe_stats,
88+
const CBlockIndex* tip,
89+
AutoFile& afile,
90+
const fs::path& path,
91+
const fs::path& temppath,
92+
const std::function<void()>& interruption_point = {});
93+
7894
/* Calculate the difficulty for a given block index.
7995
*/
8096
double GetDifficulty(const CBlockIndex& blockindex)
@@ -2776,44 +2792,59 @@ static RPCHelpMan dumptxoutset()
27762792
// would be classified as a block connecting an invalid block.
27772793
disable_network = std::make_unique<NetworkDisable>(connman);
27782794

2779-
// Note: Unlocking cs_main before CreateUTXOSnapshot might be racy
2780-
// if the user interacts with any other *block RPCs.
27812795
invalidate_index = WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Next(target_index));
27822796
InvalidateBlock(*node.chainman, invalidate_index->GetBlockHash());
2783-
const CBlockIndex* new_tip_index{WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Tip())};
2797+
}
27842798

2799+
Chainstate* chainstate;
2800+
std::unique_ptr<CCoinsViewCursor> cursor;
2801+
CCoinsStats stats;
2802+
UniValue result;
2803+
UniValue error;
2804+
{
2805+
// Lock the chainstate before calling PrepareUtxoSnapshot, to be able
2806+
// to get a UTXO database cursor while the chain is pointing at the
2807+
// target block. After that, release the lock while calling
2808+
// WriteUTXOSnapshot. The cursor will remain valid and be used by
2809+
// WriteUTXOSnapshot to write a consistent snapshot even if the
2810+
// chainstate changes.
2811+
LOCK(node.chainman->GetMutex());
2812+
chainstate = &node.chainman->ActiveChainstate();
27852813
// In case there is any issue with a block being read from disk we need
27862814
// to stop here, otherwise the dump could still be created for the wrong
27872815
// height.
27882816
// The new tip could also not be the target block if we have a stale
27892817
// sister block of invalidate_index. This block (or a descendant) would
27902818
// be activated as the new tip and we would not get to new_tip_index.
2791-
if (new_tip_index != target_index) {
2792-
ReconsiderBlock(*node.chainman, invalidate_index->GetBlockHash());
2793-
throw JSONRPCError(RPC_MISC_ERROR, "Could not roll back to requested height, reverting to tip.");
2819+
if (target_index != chainstate->m_chain.Tip()) {
2820+
LogInfo("Failed to roll back to requested height, reverting to tip.\n");
2821+
error = JSONRPCError(RPC_MISC_ERROR, "Could not roll back to requested height.");
2822+
} else {
2823+
std::tie(cursor, stats, tip) = PrepareUTXOSnapshot(*chainstate, node.rpc_interruption_point);
27942824
}
27952825
}
27962826

2797-
UniValue result = CreateUTXOSnapshot(
2798-
node, node.chainman->ActiveChainstate(), afile, path, temppath);
2799-
fs::rename(temppath, path);
2800-
2827+
if (error.isNull()) {
2828+
result = WriteUTXOSnapshot(*chainstate, cursor.get(), &stats, tip, afile, path, temppath, node.rpc_interruption_point);
2829+
fs::rename(temppath, path);
2830+
}
28012831
if (invalidate_index) {
28022832
ReconsiderBlock(*node.chainman, invalidate_index->GetBlockHash());
28032833
}
2834+
if (!error.isNull()) {
2835+
throw error;
2836+
}
28042837

28052838
result.pushKV("path", path.utf8string());
28062839
return result;
28072840
},
28082841
};
28092842
}
28102843

2811-
UniValue CreateUTXOSnapshot(
2812-
NodeContext& node,
2844+
std::tuple<std::unique_ptr<CCoinsViewCursor>, CCoinsStats, const CBlockIndex*>
2845+
PrepareUTXOSnapshot(
28132846
Chainstate& chainstate,
2814-
AutoFile& afile,
2815-
const fs::path& path,
2816-
const fs::path& temppath)
2847+
const std::function<void()>& interruption_point)
28172848
{
28182849
std::unique_ptr<CCoinsViewCursor> pcursor;
28192850
std::optional<CCoinsStats> maybe_stats;
@@ -2823,7 +2854,7 @@ UniValue CreateUTXOSnapshot(
28232854
// We need to lock cs_main to ensure that the coinsdb isn't written to
28242855
// between (i) flushing coins cache to disk (coinsdb), (ii) getting stats
28252856
// based upon the coinsdb, and (iii) constructing a cursor to the
2826-
// coinsdb for use below this block.
2857+
// coinsdb for use in WriteUTXOSnapshot.
28272858
//
28282859
// Cursors returned by leveldb iterate over snapshots, so the contents
28292860
// of the pcursor will not be affected by simultaneous writes during
@@ -2832,11 +2863,11 @@ UniValue CreateUTXOSnapshot(
28322863
// See discussion here:
28332864
// https://github.com/bitcoin/bitcoin/pull/15606#discussion_r274479369
28342865
//
2835-
LOCK(::cs_main);
2866+
AssertLockHeld(::cs_main);
28362867

28372868
chainstate.ForceFlushStateToDisk();
28382869

2839-
maybe_stats = GetUTXOStats(&chainstate.CoinsDB(), chainstate.m_blockman, CoinStatsHashType::HASH_SERIALIZED, node.rpc_interruption_point);
2870+
maybe_stats = GetUTXOStats(&chainstate.CoinsDB(), chainstate.m_blockman, CoinStatsHashType::HASH_SERIALIZED, interruption_point);
28402871
if (!maybe_stats) {
28412872
throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
28422873
}
@@ -2845,6 +2876,19 @@ UniValue CreateUTXOSnapshot(
28452876
tip = CHECK_NONFATAL(chainstate.m_blockman.LookupBlockIndex(maybe_stats->hashBlock));
28462877
}
28472878

2879+
return {std::move(pcursor), *CHECK_NONFATAL(maybe_stats), tip};
2880+
}
2881+
2882+
UniValue WriteUTXOSnapshot(
2883+
Chainstate& chainstate,
2884+
CCoinsViewCursor* pcursor,
2885+
CCoinsStats* maybe_stats,
2886+
const CBlockIndex* tip,
2887+
AutoFile& afile,
2888+
const fs::path& path,
2889+
const fs::path& temppath,
2890+
const std::function<void()>& interruption_point)
2891+
{
28482892
LOG_TIME_SECONDS(strprintf("writing UTXO snapshot at height %s (%s) to file %s (via %s)",
28492893
tip->nHeight, tip->GetBlockHash().ToString(),
28502894
fs::PathToString(path), fs::PathToString(temppath)));
@@ -2880,7 +2924,7 @@ UniValue CreateUTXOSnapshot(
28802924
pcursor->GetKey(key);
28812925
last_hash = key.hash;
28822926
while (pcursor->Valid()) {
2883-
if (iter % 5000 == 0) node.rpc_interruption_point();
2927+
if (iter % 5000 == 0) interruption_point();
28842928
++iter;
28852929
if (pcursor->GetKey(key) && pcursor->GetValue(coin)) {
28862930
if (key.hash != last_hash) {
@@ -2911,6 +2955,17 @@ UniValue CreateUTXOSnapshot(
29112955
return result;
29122956
}
29132957

2958+
UniValue CreateUTXOSnapshot(
2959+
node::NodeContext& node,
2960+
Chainstate& chainstate,
2961+
AutoFile& afile,
2962+
const fs::path& path,
2963+
const fs::path& tmppath)
2964+
{
2965+
auto [cursor, stats, tip]{WITH_LOCK(::cs_main, return PrepareUTXOSnapshot(chainstate, node.rpc_interruption_point))};
2966+
return WriteUTXOSnapshot(chainstate, cursor.get(), &stats, tip, afile, path, tmppath, node.rpc_interruption_point);
2967+
}
2968+
29142969
static RPCHelpMan loadtxoutset()
29152970
{
29162971
return RPCHelpMan{

src/rpc/blockchain.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ UniValue blockheaderToJSON(const CBlockIndex& tip, const CBlockIndex& blockindex
4848
void CalculatePercentilesByWeight(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector<std::pair<CAmount, int64_t>>& scores, int64_t total_weight);
4949

5050
/**
51-
* Helper to create UTXO snapshots given a chainstate and a file handle.
51+
* Test-only helper to create UTXO snapshots given a chainstate and a file handle.
5252
* @return a UniValue map containing metadata about the snapshot.
5353
*/
5454
UniValue CreateUTXOSnapshot(

src/validation.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,7 @@ class ChainstateManager
914914
//! Internal helper for ActivateSnapshot().
915915
//!
916916
//! De-serialization of a snapshot that is created with
917-
//! CreateUTXOSnapshot() in rpc/blockchain.cpp.
917+
//! the dumptxoutset RPC.
918918
//! To reduce space the serialization format of the snapshot avoids
919919
//! duplication of tx hashes. The code takes advantage of the guarantee by
920920
//! leveldb that keys are lexicographically sorted.

0 commit comments

Comments
 (0)