Skip to content

Commit 18f9984

Browse files
committed
Merge bitcoin#25667: assumeutxo: snapshot initialization
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9cf919c commit 18f9984

16 files changed

Lines changed: 354 additions & 25 deletions

doc/design/assumeutxo.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ original chainstate remains in use as active.
7676

7777
Once the snapshot chainstate is loaded and validated, it is promoted to active
7878
chainstate and a sync to tip begins. A new chainstate directory is created in the
79-
datadir for the snapshot chainstate called
80-
`chainstate_[SHA256 blockhash of snapshot base block]`.
79+
datadir for the snapshot chainstate called `chainstate_snapshot`. When this directory
80+
is present in the datadir, the snapshot chainstate will be detected and loaded as
81+
active on node startup (via `DetectSnapshotChainstate()`).
8182

8283
| | |
8384
| ---------- | ----------- |

src/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ libbitcoin_node_a_SOURCES = \
569569
node/psbt.cpp \
570570
node/transaction.cpp \
571571
node/txreconciliation.cpp \
572+
node/utxo_snapshot.cpp \
572573
node/interface_ui.cpp \
573574
noui.cpp \
574575
policy/fees.cpp \

src/dbwrapper.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ static leveldb::Options GetOptions(size_t nCacheSize)
127127
}
128128

129129
CDBWrapper::CDBWrapper(const fs::path& path, size_t nCacheSize, bool fMemory, bool fWipe, bool obfuscate)
130-
: m_name{fs::PathToString(path.stem())}
130+
: m_name{fs::PathToString(path.stem())}, m_path{path}, m_is_memory{fMemory}
131131
{
132132
penv = nullptr;
133133
readoptions.verify_checksums = true;

src/dbwrapper.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class dbwrapper_error : public std::runtime_error
5151

5252
class CDBWrapper;
5353

54+
namespace dbwrapper {
55+
using leveldb::DestroyDB;
56+
}
57+
5458
/** These should be considered an implementation detail of the specific database.
5559
*/
5660
namespace dbwrapper_private {
@@ -250,6 +254,12 @@ class CDBWrapper
250254

251255
std::vector<unsigned char> CreateObfuscateKey() const;
252256

257+
//! path to filesystem storage
258+
const fs::path m_path;
259+
260+
//! whether or not the database resides in memory
261+
bool m_is_memory;
262+
253263
public:
254264
/**
255265
* @param[in] path Location in the filesystem where leveldb data will be stored.
@@ -325,6 +335,14 @@ class CDBWrapper
325335
return WriteBatch(batch, fSync);
326336
}
327337

338+
//! @returns filesystem path to the on-disk data.
339+
std::optional<fs::path> StoragePath() {
340+
if (m_is_memory) {
341+
return {};
342+
}
343+
return m_path;
344+
}
345+
328346
template <typename K>
329347
bool Exists(const K& key) const
330348
{

src/node/blockstorage.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,16 @@ void BlockManager::FlushUndoFile(int block_file, bool finalize)
557557
void BlockManager::FlushBlockFile(bool fFinalize, bool finalize_undo)
558558
{
559559
LOCK(cs_LastBlockFile);
560+
561+
if (m_blockfile_info.size() < 1) {
562+
// Return if we haven't loaded any blockfiles yet. This happens during
563+
// chainstate init, when we call ChainstateManager::MaybeRebalanceCaches() (which
564+
// then calls FlushStateToDisk()), resulting in a call to this function before we
565+
// have populated `m_blockfile_info` via LoadBlockIndexDB().
566+
return;
567+
}
568+
assert(static_cast<int>(m_blockfile_info.size()) > m_last_blockfile);
569+
560570
FlatFilePos block_pos_old(m_last_blockfile, m_blockfile_info[m_last_blockfile].nSize);
561571
if (!BlockFileSeq().Flush(block_pos_old, fFinalize)) {
562572
AbortNode("Flushing block file to disk failed. This is likely the result of an I/O error.");

src/node/chainstate.cpp

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,15 @@ std::optional<ChainstateLoadingError> LoadChainstate(bool fReset,
7979
mnhf_manager.reset();
8080
mnhf_manager = std::make_unique<CMNHFManager>(*evodb);
8181

82-
chainman.InitializeChainstate(mempool, *evodb, chain_helper);
8382
chainman.m_total_coinstip_cache = nCoinCacheUsage;
8483
chainman.m_total_coinsdb_cache = nCoinDBCache;
8584

85+
// Load the fully validated chainstate.
86+
chainman.InitializeChainstate(mempool, *evodb, chain_helper);
87+
88+
// Load a chain created from a UTXO snapshot, if any exist.
89+
chainman.DetectSnapshotChainstate(mempool, *evodb, chain_helper);
90+
8691
auto& pblocktree{chainman.m_blockman.m_block_tree_db};
8792
// new CBlockTreeDB tries to delete the existing file, which
8893
// fails if it's still open from the previous loop. Close it first:
@@ -160,12 +165,20 @@ std::optional<ChainstateLoadingError> LoadChainstate(bool fReset,
160165
return ChainstateLoadingError::ERROR_LOAD_GENESIS_BLOCK_FAILED;
161166
}
162167

168+
// Conservative value which is arbitrarily chosen, as it will ultimately be changed
169+
// by a call to `chainman.MaybeRebalanceCaches()`. We just need to make sure
170+
// that the sum of the two caches (40%) does not exceed the allowable amount
171+
// during this temporary initialization state.
172+
double init_cache_fraction = 0.2;
173+
163174
// At this point we're either in reindex or we've loaded a useful
164175
// block tree into BlockIndex()!
165176

166177
for (CChainState* chainstate : chainman.GetAll()) {
178+
LogPrintf("Initializing chainstate %s\n", chainstate->ToString());
179+
167180
chainstate->InitCoinsDB(
168-
/*cache_size_bytes=*/nCoinDBCache,
181+
/*cache_size_bytes=*/chainman.m_total_coinsdb_cache * init_cache_fraction,
169182
/*in_memory=*/coins_db_in_memory,
170183
/*should_wipe=*/fReset || fReindexChainState);
171184

@@ -185,7 +198,7 @@ std::optional<ChainstateLoadingError> LoadChainstate(bool fReset,
185198
}
186199

187200
// The on-disk coinsdb is now in a good state, create the cache
188-
chainstate->InitCoinsCache(nCoinCacheUsage);
201+
chainstate->InitCoinsCache(chainman.m_total_coinstip_cache * init_cache_fraction);
189202
assert(chainstate->CanFlushToDisk());
190203

191204
// flush evodb
@@ -214,6 +227,11 @@ std::optional<ChainstateLoadingError> LoadChainstate(bool fReset,
214227
return ChainstateLoadingError::ERROR_UPGRADING_EVO_DB;
215228
}
216229

230+
// Now that chainstates are loaded and we're able to flush to
231+
// disk, rebalance the coins caches to desired levels based
232+
// on the condition of each chainstate.
233+
chainman.MaybeRebalanceCaches();
234+
217235
return std::nullopt;
218236
}
219237

src/node/utxo_snapshot.cpp

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) 2022 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <node/utxo_snapshot.h>
6+
7+
#include <fs.h>
8+
#include <logging.h>
9+
#include <streams.h>
10+
#include <uint256.h>
11+
#include <util/system.h>
12+
#include <validation.h>
13+
14+
#include <cstdio>
15+
#include <optional>
16+
17+
namespace node {
18+
19+
bool WriteSnapshotBaseBlockhash(CChainState& snapshot_chainstate)
20+
{
21+
AssertLockHeld(::cs_main);
22+
assert(snapshot_chainstate.m_from_snapshot_blockhash);
23+
24+
const std::optional<fs::path> chaindir = snapshot_chainstate.CoinsDB().StoragePath();
25+
assert(chaindir); // Sanity check that chainstate isn't in-memory.
26+
const fs::path write_to = *chaindir / node::SNAPSHOT_BLOCKHASH_FILENAME;
27+
28+
FILE* file{fsbridge::fopen(write_to, "wb")};
29+
AutoFile afile{file};
30+
if (afile.IsNull()) {
31+
LogPrintf("[snapshot] failed to open base blockhash file for writing: %s\n",
32+
fs::PathToString(write_to));
33+
return false;
34+
}
35+
afile << *snapshot_chainstate.m_from_snapshot_blockhash;
36+
37+
if (afile.fclose() != 0) {
38+
LogPrintf("[snapshot] failed to close base blockhash file %s after writing\n",
39+
fs::PathToString(write_to));
40+
return false;
41+
}
42+
return true;
43+
}
44+
45+
std::optional<uint256> ReadSnapshotBaseBlockhash(fs::path chaindir)
46+
{
47+
if (!fs::exists(chaindir)) {
48+
LogPrintf("[snapshot] cannot read base blockhash: no chainstate dir " /* Continued */
49+
"exists at path %s\n", fs::PathToString(chaindir));
50+
return std::nullopt;
51+
}
52+
const fs::path read_from = chaindir / node::SNAPSHOT_BLOCKHASH_FILENAME;
53+
const std::string read_from_str = fs::PathToString(read_from);
54+
55+
if (!fs::exists(read_from)) {
56+
LogPrintf("[snapshot] snapshot chainstate dir is malformed! no base blockhash file " /* Continued */
57+
"exists at path %s. Try deleting %s and calling loadtxoutset again?\n",
58+
fs::PathToString(chaindir), read_from_str);
59+
return std::nullopt;
60+
}
61+
62+
uint256 base_blockhash;
63+
FILE* file{fsbridge::fopen(read_from, "rb")};
64+
AutoFile afile{file};
65+
if (afile.IsNull()) {
66+
LogPrintf("[snapshot] failed to open base blockhash file for reading: %s\n",
67+
read_from_str);
68+
return std::nullopt;
69+
}
70+
afile >> base_blockhash;
71+
72+
if (std::fgetc(afile.Get()) != EOF) {
73+
LogPrintf("[snapshot] warning: unexpected trailing data in %s\n", read_from_str);
74+
} else if (std::ferror(afile.Get())) {
75+
LogPrintf("[snapshot] warning: i/o error reading %s\n", read_from_str);
76+
}
77+
return base_blockhash;
78+
}
79+
80+
std::optional<fs::path> FindSnapshotChainstateDir()
81+
{
82+
fs::path possible_dir =
83+
gArgs.GetDataDirNet() / fs::u8path(strprintf("chainstate%s", SNAPSHOT_CHAINSTATE_SUFFIX));
84+
85+
if (fs::exists(possible_dir)) {
86+
return possible_dir;
87+
}
88+
return std::nullopt;
89+
}
90+
91+
} // namespace node

src/node/utxo_snapshot.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@
66
#ifndef BITCOIN_NODE_UTXO_SNAPSHOT_H
77
#define BITCOIN_NODE_UTXO_SNAPSHOT_H
88

9+
#include <fs.h>
910
#include <uint256.h>
1011
#include <serialize.h>
12+
#include <validation.h>
13+
14+
#include <optional>
15+
16+
extern RecursiveMutex cs_main;
1117

1218
namespace node {
1319
//! Metadata describing a serialized version of a UTXO set from which an
@@ -33,6 +39,33 @@ class SnapshotMetadata
3339

3440
SERIALIZE_METHODS(SnapshotMetadata, obj) { READWRITE(obj.m_base_blockhash, obj.m_coins_count); }
3541
};
42+
43+
//! The file in the snapshot chainstate dir which stores the base blockhash. This is
44+
//! needed to reconstruct snapshot chainstates on init.
45+
//!
46+
//! Because we only allow loading a single snapshot at a time, there will only be one
47+
//! chainstate directory with this filename present within it.
48+
const fs::path SNAPSHOT_BLOCKHASH_FILENAME{"base_blockhash"};
49+
50+
//! Write out the blockhash of the snapshot base block that was used to construct
51+
//! this chainstate. This value is read in during subsequent initializations and
52+
//! used to reconstruct snapshot-based chainstates.
53+
bool WriteSnapshotBaseBlockhash(CChainState& snapshot_chainstate)
54+
EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
55+
56+
//! Read the blockhash of the snapshot base block that was used to construct the
57+
//! chainstate.
58+
std::optional<uint256> ReadSnapshotBaseBlockhash(fs::path chaindir)
59+
EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
60+
61+
//! Suffix appended to the chainstate (leveldb) dir when created based upon
62+
//! a snapshot.
63+
constexpr std::string_view SNAPSHOT_CHAINSTATE_SUFFIX = "_snapshot";
64+
65+
66+
//! Return a path to the snapshot-based chainstate dir, if one exists.
67+
std::optional<fs::path> FindSnapshotChainstateDir();
68+
3669
} // namespace node
3770

3871
#endif // BITCOIN_NODE_UTXO_SNAPSHOT_H

src/streams.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,12 +507,14 @@ class AutoFile
507507
AutoFile(const AutoFile&) = delete;
508508
AutoFile& operator=(const AutoFile&) = delete;
509509

510-
void fclose()
510+
int fclose()
511511
{
512+
int retval{0};
512513
if (file) {
513-
::fclose(file);
514+
retval = ::fclose(file);
514515
file = nullptr;
515516
}
517+
return retval;
516518
}
517519

518520
/** Get wrapped FILE* with transfer of ownership.

src/test/util/setup_common.cpp

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,7 @@ ChainTestingSetup::~ChainTestingSetup()
296296
m_node.scheduler.reset();
297297
}
298298

299-
TestingSetup::TestingSetup(const std::string& chainName, const std::vector<const char*>& extra_args)
300-
: ChainTestingSetup(chainName, extra_args)
299+
void TestingSetup::LoadVerifyActivateChainstate()
301300
{
302301
const CChainParams& chainparams = Params();
303302
// Ideally we'd move all the RPC tests to the functional testing framework
@@ -331,8 +330,8 @@ TestingSetup::TestingSetup(const std::string& chainName, const std::vector<const
331330
m_cache_sizes.block_tree_db,
332331
m_cache_sizes.coins_db,
333332
m_cache_sizes.coins,
334-
/*block_tree_db_in_memory=*/true,
335-
/*coins_db_in_memory=*/true,
333+
/*block_tree_db_in_memory=*/m_block_tree_db_in_memory,
334+
/*coins_db_in_memory=*/m_coins_db_in_memory,
336335
/*dash_dbs_in_memory=*/true);
337336
assert(!maybe_load_error.has_value());
338337

@@ -386,6 +385,18 @@ TestingSetup::TestingSetup(const std::string& chainName, const std::vector<const
386385
}
387386
}
388387

388+
TestingSetup::TestingSetup(
389+
const std::string& chainName,
390+
const std::vector<const char*>& extra_args,
391+
const bool coins_db_in_memory,
392+
const bool block_tree_db_in_memory)
393+
: ChainTestingSetup(chainName, extra_args),
394+
m_coins_db_in_memory(coins_db_in_memory),
395+
m_block_tree_db_in_memory(block_tree_db_in_memory)
396+
{
397+
LoadVerifyActivateChainstate();
398+
}
399+
389400
TestingSetup::~TestingSetup()
390401
{
391402
m_node.peerman.reset();
@@ -414,13 +425,22 @@ TestingSetup::~TestingSetup()
414425
DashChainstateSetupClose(m_node);
415426
}
416427

417-
TestChain100Setup::TestChain100Setup(const std::string& chain_name, const std::vector<const char*>& extra_args)
418-
: TestChainSetup{100, chain_name, extra_args}
428+
TestChain100Setup::TestChain100Setup(
429+
const std::string& chain_name,
430+
const std::vector<const char*>& extra_args,
431+
const bool coins_db_in_memory,
432+
const bool block_tree_db_in_memory)
433+
: TestChainSetup{100, chain_name, extra_args, coins_db_in_memory, block_tree_db_in_memory}
419434
{
420435
}
421436

422-
TestChainSetup::TestChainSetup(int num_blocks, const std::string& chain_name, const std::vector<const char*>& extra_args)
423-
: TestingSetup{chain_name, extra_args}
437+
TestChainSetup::TestChainSetup(
438+
int num_blocks,
439+
const std::string& chain_name,
440+
const std::vector<const char*>& extra_args,
441+
const bool coins_db_in_memory,
442+
const bool block_tree_db_in_memory)
443+
: TestingSetup{chain_name, extra_args, coins_db_in_memory, block_tree_db_in_memory}
424444
{
425445
SetMockTime(1598887952);
426446
constexpr std::array<unsigned char, 32> vchKey = {

0 commit comments

Comments
 (0)