mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-05-12 06:53:11 +02:00
Merge bitcoin/bitcoin#33477: Rollback for dumptxoutset without invalidating blocks
fc736013a5rpc: Add in_memory option to dumptxoutset with rollback (Fabian Jahr)d0fd718948test: Extend named pipe sqlite tool test to use rollback (Fabian Jahr)ab9463efactest: Add dumptxoutset fork test (Fabian Jahr)49d5e835a8rpc: Don't invalidate blocks in dumptxoutset (Fabian Jahr)fe58eb9850blockstorage: Add DeletePruneLock (Fabian Jahr) Pull request description: This is an alternative approach to implement `dumptxoutset` with rollback that was discussed a few times. It does not rely on `invalidateblock` and `reconsiderblock` and instead creates a temporary copy of the coins DB, modifies this copy by rolling back as many blocks as necessary and then creating the dump from this temp copy DB. See also https://github.com/bitcoin/bitcoin/pull/29553#issuecomment-1978480989, https://github.com/bitcoin/bitcoin/issues/32817#issuecomment-3012406102 and #29565 discussions. The nice side-effects of this are that forks can not interfere with the rollback and network activity does not have to be suspended. But there are also some downsides when comparing to the current approach: this does require some additional disk space for the copied coins DB and performance is slower (master took 3m 17s vs 9m 16s in my last test with the code here, rolling back ~1500 blocks). However, there is also not much code being added here, network can stay active throughout and performance would stay constant with this approach while it would impact master if there were forks that needed to be invalidated as well (see #33444 for the alternative approach), so this could still be considered a good trade-off. ACKs for top commit: stratospher: tested ACKfc73601. very nice! sedited: Re-ACKfc736013a5theStack: re-ACKfc736013a5Tree-SHA512: d3d674f68184ac3ada87d969d0fca7bc38203ee939853864adcd235ee3a954914c7e351b817800b885a495606e323392c27d88ba8d8e018eaf8567c098eb0e9c
This commit is contained in:
@@ -404,6 +404,12 @@ void BlockManager::UpdatePruneLock(const std::string& name, const PruneLockInfo&
|
||||
m_prune_locks[name] = lock_info;
|
||||
}
|
||||
|
||||
bool BlockManager::DeletePruneLock(const std::string& name)
|
||||
{
|
||||
AssertLockHeld(::cs_main);
|
||||
return m_prune_locks.erase(name) > 0;
|
||||
}
|
||||
|
||||
CBlockIndex* BlockManager::InsertBlockIndex(const uint256& hash)
|
||||
{
|
||||
AssertLockHeld(cs_main);
|
||||
|
||||
@@ -456,6 +456,9 @@ public:
|
||||
//! Create or update a prune lock identified by its name
|
||||
void UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
|
||||
|
||||
//! Delete a prune lock identified by its name. Returns true if the lock existed.
|
||||
bool DeletePruneLock(const std::string& name) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
|
||||
|
||||
/** Open a block file (blk?????.dat) */
|
||||
AutoFile OpenBlockFile(const FlatFilePos& pos, bool fReadOnly) const;
|
||||
|
||||
|
||||
@@ -91,6 +91,15 @@ UniValue WriteUTXOSnapshot(
|
||||
const fs::path& temppath,
|
||||
const std::function<void()>& interruption_point = {});
|
||||
|
||||
UniValue CreateRolledBackUTXOSnapshot(
|
||||
NodeContext& node,
|
||||
Chainstate& chainstate,
|
||||
const CBlockIndex* target,
|
||||
AutoFile&& afile,
|
||||
const fs::path& path,
|
||||
const fs::path& tmppath,
|
||||
bool in_memory);
|
||||
|
||||
/* Calculate the difficulty for a given block index.
|
||||
*/
|
||||
double GetDifficulty(const CBlockIndex& blockindex)
|
||||
@@ -3035,39 +3044,26 @@ static RPCMethod getblockfilter()
|
||||
}
|
||||
|
||||
/**
|
||||
* RAII class that disables the network in its constructor and enables it in its
|
||||
* destructor.
|
||||
* RAII class that registers a prune lock in its constructor to prevent
|
||||
* block data from being pruned, and removes it in its destructor.
|
||||
*/
|
||||
class NetworkDisable
|
||||
class TemporaryPruneLock
|
||||
{
|
||||
CConnman& m_connman;
|
||||
static constexpr const char* LOCK_NAME{"dumptxoutset-rollback"};
|
||||
BlockManager& m_blockman;
|
||||
public:
|
||||
NetworkDisable(CConnman& connman) : m_connman(connman) {
|
||||
m_connman.SetNetworkActive(false);
|
||||
if (m_connman.GetNetworkActive()) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Network activity could not be suspended.");
|
||||
}
|
||||
};
|
||||
~NetworkDisable() {
|
||||
m_connman.SetNetworkActive(true);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* RAII class that temporarily rolls back the local chain in it's constructor
|
||||
* and rolls it forward again in it's destructor.
|
||||
*/
|
||||
class TemporaryRollback
|
||||
{
|
||||
ChainstateManager& m_chainman;
|
||||
const CBlockIndex& m_invalidate_index;
|
||||
public:
|
||||
TemporaryRollback(ChainstateManager& chainman, const CBlockIndex& index) : m_chainman(chainman), m_invalidate_index(index) {
|
||||
InvalidateBlock(m_chainman, m_invalidate_index.GetBlockHash());
|
||||
};
|
||||
~TemporaryRollback() {
|
||||
ReconsiderBlock(m_chainman, m_invalidate_index.GetBlockHash());
|
||||
};
|
||||
TemporaryPruneLock(BlockManager& blockman, int height) : m_blockman(blockman)
|
||||
{
|
||||
LOCK(::cs_main);
|
||||
m_blockman.UpdatePruneLock(LOCK_NAME, {height});
|
||||
LogDebug(BCLog::PRUNE, "dumptxoutset: registered prune lock at height %d", height);
|
||||
}
|
||||
~TemporaryPruneLock()
|
||||
{
|
||||
LOCK(::cs_main);
|
||||
m_blockman.DeletePruneLock(LOCK_NAME);
|
||||
LogDebug(BCLog::PRUNE, "dumptxoutset: released prune lock");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -3079,10 +3075,9 @@ static RPCMethod dumptxoutset()
|
||||
{
|
||||
return RPCMethod{
|
||||
"dumptxoutset",
|
||||
"Write the serialized UTXO set to a file. This can be used in loadtxoutset afterwards if this snapshot height is supported in the chainparams as well.\n\n"
|
||||
"Unless the \"latest\" type is requested, the node will roll back to the requested height and network activity will be suspended during this process. "
|
||||
"Because of this it is discouraged to interact with the node in any other way during the execution of this call to avoid inconsistent results and race conditions, particularly RPCs that interact with blockstorage.\n\n"
|
||||
"This call may take several minutes. Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
|
||||
"Write the serialized UTXO set to a file. This can be used in loadtxoutset afterwards if this snapshot height is supported in the chainparams as well.\n"
|
||||
"This creates a temporary UTXO database when rolling back, keeping the main chain intact. Should the node experience an unclean shutdown the temporary database may need to be removed from the datadir manually.\n"
|
||||
"For deep rollbacks, make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0) as it may take several minutes.",
|
||||
{
|
||||
{"path", RPCArg::Type::STR, RPCArg::Optional::NO, "Path to the output file. If relative, will be prefixed by datadir."},
|
||||
{"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 can currently be loaded with loadtxoutset."},
|
||||
@@ -3091,6 +3086,7 @@ static RPCMethod dumptxoutset()
|
||||
{"rollback", RPCArg::Type::NUM, RPCArg::Optional::OMITTED,
|
||||
"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.",
|
||||
RPCArgOptions{.skip_type_check = true, .type_str = {"", "string or numeric"}}},
|
||||
{"in_memory", RPCArg::Type::BOOL, RPCArg::Default{false}, "If true, the temporary UTXO-set database used during rollback is kept entirely in memory. This can significantly speed up the process but requires sufficient free RAM (over 10 GB on mainnet)."},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -3108,7 +3104,8 @@ static RPCMethod dumptxoutset()
|
||||
RPCExamples{
|
||||
HelpExampleCli("-rpcclienttimeout=0 dumptxoutset", "utxo.dat latest") +
|
||||
HelpExampleCli("-rpcclienttimeout=0 dumptxoutset", "utxo.dat rollback") +
|
||||
HelpExampleCli("-rpcclienttimeout=0 -named dumptxoutset", R"(utxo.dat rollback=853456)")
|
||||
HelpExampleCli("-rpcclienttimeout=0 -named dumptxoutset", R"(utxo.dat rollback=853456)") +
|
||||
HelpExampleCli("-rpcclienttimeout=0 -named dumptxoutset", R"(utxo.dat rollback=853456 in_memory=true)")
|
||||
},
|
||||
[](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
@@ -3155,16 +3152,15 @@ static RPCMethod dumptxoutset()
|
||||
"Couldn't open file " + temppath.utf8string() + " for writing.");
|
||||
}
|
||||
|
||||
CConnman& connman = EnsureConnman(node);
|
||||
const CBlockIndex* invalidate_index{nullptr};
|
||||
std::optional<NetworkDisable> disable_network;
|
||||
std::optional<TemporaryRollback> temporary_rollback;
|
||||
|
||||
// If the user wants to dump the txoutset of the current tip, we don't have
|
||||
// to roll back at all
|
||||
if (target_index != tip) {
|
||||
// If the node is running in pruned mode we ensure all necessary block
|
||||
// data is available before starting to roll back.
|
||||
UniValue result;
|
||||
Chainstate& chainstate{node.chainman->ActiveChainstate()};
|
||||
if (target_index == tip) {
|
||||
// Dump the txoutset of the current tip
|
||||
result = CreateUTXOSnapshot(node, chainstate, std::move(afile), path, temppath);
|
||||
} else {
|
||||
// Check pruning constraints before attempting rollback and prevent
|
||||
// pruning of the necessary blocks with a temporary prune lock
|
||||
std::optional<TemporaryPruneLock> temp_prune_lock;
|
||||
if (node.chainman->m_blockman.IsPruneMode()) {
|
||||
LOCK(node.chainman->GetMutex());
|
||||
const CBlockIndex* current_tip{node.chainman->ActiveChain().Tip()};
|
||||
@@ -3172,68 +3168,188 @@ static RPCMethod dumptxoutset()
|
||||
if (first_block.nHeight > target_index->nHeight) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Could not roll back to requested height since necessary block data is already pruned.");
|
||||
}
|
||||
temp_prune_lock.emplace(node.chainman->m_blockman, target_index->nHeight);
|
||||
}
|
||||
|
||||
// Suspend network activity for the duration of the process when we are
|
||||
// rolling back the chain to get a utxo set from a past height. We do
|
||||
// this so we don't punish peers that send us that send us data that
|
||||
// seems wrong in this temporary state. For example a normal new block
|
||||
// would be classified as a block connecting an invalid block.
|
||||
// Skip if the network is already disabled because this
|
||||
// automatically re-enables the network activity at the end of the
|
||||
// process which may not be what the user wants.
|
||||
if (connman.GetNetworkActive()) {
|
||||
disable_network.emplace(connman);
|
||||
}
|
||||
|
||||
invalidate_index = WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Next(target_index));
|
||||
temporary_rollback.emplace(*node.chainman, *invalidate_index);
|
||||
const bool in_memory{options.exists("in_memory") ? options["in_memory"].get_bool() : false};
|
||||
result = CreateRolledBackUTXOSnapshot(node,
|
||||
chainstate,
|
||||
target_index,
|
||||
std::move(afile),
|
||||
path,
|
||||
temppath,
|
||||
in_memory);
|
||||
}
|
||||
|
||||
Chainstate* chainstate;
|
||||
std::unique_ptr<CCoinsViewCursor> cursor;
|
||||
CCoinsStats stats;
|
||||
{
|
||||
// Lock the chainstate before calling PrepareUtxoSnapshot, to be able
|
||||
// to get a UTXO database cursor while the chain is pointing at the
|
||||
// target block. After that, release the lock while calling
|
||||
// WriteUTXOSnapshot. The cursor will remain valid and be used by
|
||||
// WriteUTXOSnapshot to write a consistent snapshot even if the
|
||||
// chainstate changes.
|
||||
LOCK(node.chainman->GetMutex());
|
||||
chainstate = &node.chainman->ActiveChainstate();
|
||||
// In case there is any issue with a block being read from disk we need
|
||||
// to stop here, otherwise the dump could still be created for the wrong
|
||||
// height.
|
||||
// The new tip could also not be the target block if we have a stale
|
||||
// sister block of invalidate_index. This block (or a descendant) would
|
||||
// be activated as the new tip and we would not get to new_tip_index.
|
||||
if (target_index != chainstate->m_chain.Tip()) {
|
||||
LogWarning("dumptxoutset failed to roll back to requested height, reverting to tip.\n");
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Could not roll back to requested height.");
|
||||
} else {
|
||||
std::tie(cursor, stats, tip) = PrepareUTXOSnapshot(*chainstate, node.rpc_interruption_point);
|
||||
}
|
||||
}
|
||||
|
||||
UniValue result = WriteUTXOSnapshot(*chainstate,
|
||||
cursor.get(),
|
||||
&stats,
|
||||
tip,
|
||||
std::move(afile),
|
||||
path,
|
||||
temppath,
|
||||
node.rpc_interruption_point);
|
||||
if (!fs::is_fifo(path_info)) {
|
||||
fs::rename(temppath, path);
|
||||
}
|
||||
|
||||
result.pushKV("path", path.utf8string());
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* RAII class that creates a temporary database directory in its constructor
|
||||
* and removes it in its destructor.
|
||||
*/
|
||||
class TemporaryUTXODatabase
|
||||
{
|
||||
fs::path m_path;
|
||||
public:
|
||||
TemporaryUTXODatabase(const fs::path& path) : m_path(path) {
|
||||
fs::create_directories(m_path);
|
||||
}
|
||||
~TemporaryUTXODatabase() {
|
||||
if (!DestroyDB(fs::PathToString(m_path))) {
|
||||
LogInfo("Failed to clean up temporary UTXO database at %s, please remove it manually.",
|
||||
fs::PathToString(m_path));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
UniValue CreateRolledBackUTXOSnapshot(
|
||||
NodeContext& node,
|
||||
Chainstate& chainstate,
|
||||
const CBlockIndex* target,
|
||||
AutoFile&& afile,
|
||||
const fs::path& path,
|
||||
const fs::path& tmppath,
|
||||
const bool in_memory)
|
||||
{
|
||||
// Create a temporary leveldb to store the UTXO set that is being rolled back
|
||||
std::string temp_db_name{strprintf("temp_utxo_%d", target->nHeight)};
|
||||
fs::path temp_db_path{fsbridge::AbsPathJoin(tmppath.parent_path(), fs::u8path(temp_db_name))};
|
||||
|
||||
// Only create the on-disk temp directory when not using in-memory mode
|
||||
std::optional<TemporaryUTXODatabase> temp_db_cleaner;
|
||||
if (!in_memory) {
|
||||
temp_db_cleaner.emplace(temp_db_path);
|
||||
} else {
|
||||
LogInfo("Using in-memory database for UTXO-set rollback (this may require significant RAM).");
|
||||
}
|
||||
|
||||
// Create temporary database
|
||||
DBParams db_params{
|
||||
.path = temp_db_path,
|
||||
.cache_bytes = 0,
|
||||
.memory_only = in_memory,
|
||||
.wipe_data = true,
|
||||
.obfuscate = false,
|
||||
.options = DBOptions{}
|
||||
};
|
||||
|
||||
std::unique_ptr<CCoinsViewDB> temp_db = std::make_unique<CCoinsViewDB>(
|
||||
std::move(db_params),
|
||||
CoinsViewOptions{}
|
||||
);
|
||||
|
||||
const CBlockIndex* tip = nullptr;
|
||||
LogInfo("Copying current UTXO set to temporary database.");
|
||||
{
|
||||
CCoinsViewCache temp_cache(temp_db.get());
|
||||
std::unique_ptr<CCoinsViewCursor> cursor;
|
||||
{
|
||||
LOCK(::cs_main);
|
||||
tip = chainstate.m_chain.Tip();
|
||||
chainstate.ForceFlushStateToDisk(/*wipe_cache=*/false);
|
||||
cursor = chainstate.CoinsDB().Cursor();
|
||||
}
|
||||
temp_cache.SetBestBlock(tip->GetBlockHash());
|
||||
|
||||
size_t coins_count = 0;
|
||||
while (cursor->Valid()) {
|
||||
node.rpc_interruption_point();
|
||||
|
||||
COutPoint key;
|
||||
Coin coin;
|
||||
if (cursor->GetKey(key) && cursor->GetValue(coin)) {
|
||||
temp_cache.AddCoin(key, std::move(coin), false);
|
||||
coins_count++;
|
||||
|
||||
// Log every 10M coins (optimized for mainnet)
|
||||
if (coins_count % 10'000'000 == 0) {
|
||||
LogInfo("Copying UTXO set: %uM coins copied.", coins_count / 1'000'000);
|
||||
}
|
||||
|
||||
// Flush periodically
|
||||
if (coins_count % 100'000 == 0) {
|
||||
temp_cache.Flush();
|
||||
}
|
||||
}
|
||||
cursor->Next();
|
||||
}
|
||||
|
||||
temp_cache.Flush();
|
||||
LogInfo("UTXO set copy complete: %u coins total", coins_count);
|
||||
}
|
||||
|
||||
LogInfo("Rolling back from height %d to %d", tip->nHeight, target->nHeight);
|
||||
|
||||
const CBlockIndex* block_index{tip};
|
||||
const size_t total_blocks{static_cast<size_t>(block_index->nHeight - target->nHeight)};
|
||||
CCoinsViewCache rollback_cache(temp_db.get());
|
||||
rollback_cache.SetBestBlock(block_index->GetBlockHash());
|
||||
size_t blocks_processed = 0;
|
||||
int last_progress{0};
|
||||
DisconnectResult res;
|
||||
|
||||
while (block_index->nHeight > target->nHeight) {
|
||||
node.rpc_interruption_point();
|
||||
|
||||
CBlock block;
|
||||
if (!node.chainman->m_blockman.ReadBlock(block, *block_index)) {
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR,
|
||||
strprintf("Failed to read block at height %d", block_index->nHeight));
|
||||
}
|
||||
|
||||
WITH_LOCK(::cs_main, res = chainstate.DisconnectBlock(block, block_index, rollback_cache));
|
||||
if (res == DISCONNECT_FAILED) {
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR,
|
||||
strprintf("Failed to roll back block at height %d", block_index->nHeight));
|
||||
}
|
||||
|
||||
blocks_processed++;
|
||||
int progress{static_cast<int>(blocks_processed * 100 / total_blocks)};
|
||||
if (progress >= last_progress + 5) {
|
||||
LogInfo("Rolled back %d%% of blocks.", progress);
|
||||
last_progress = progress;
|
||||
rollback_cache.Flush();
|
||||
}
|
||||
|
||||
block_index = block_index->pprev;
|
||||
}
|
||||
|
||||
CHECK_NONFATAL(rollback_cache.GetBestBlock() == target->GetBlockHash());
|
||||
rollback_cache.Flush();
|
||||
|
||||
LogInfo("Rollback complete. Computing UTXO statistics for created txoutset dump.");
|
||||
std::optional<CCoinsStats> maybe_stats = GetUTXOStats(temp_db.get(),
|
||||
chainstate.m_blockman,
|
||||
CoinStatsHashType::HASH_SERIALIZED,
|
||||
node.rpc_interruption_point);
|
||||
|
||||
if (!maybe_stats) {
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to compute UTXO statistics");
|
||||
}
|
||||
|
||||
std::unique_ptr<CCoinsViewCursor> pcursor{temp_db->Cursor()};
|
||||
if (!pcursor) {
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to create UTXO cursor");
|
||||
}
|
||||
|
||||
LogInfo("Writing snapshot to disk.");
|
||||
return WriteUTXOSnapshot(chainstate,
|
||||
pcursor.get(),
|
||||
&(*maybe_stats),
|
||||
target,
|
||||
std::move(afile),
|
||||
path,
|
||||
tmppath,
|
||||
node.rpc_interruption_point);
|
||||
}
|
||||
|
||||
std::tuple<std::unique_ptr<CCoinsViewCursor>, CCoinsStats, const CBlockIndex*>
|
||||
PrepareUTXOSnapshot(
|
||||
Chainstate& chainstate,
|
||||
|
||||
@@ -47,7 +47,7 @@ UniValue blockheaderToJSON(const CBlockIndex& tip, const CBlockIndex& blockindex
|
||||
void CalculatePercentilesByWeight(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector<std::pair<CAmount, int64_t>>& scores, int64_t total_weight);
|
||||
|
||||
/**
|
||||
* Test-only helper to create UTXO snapshots given a chainstate and a file handle.
|
||||
* Helper to create UTXO snapshots given a chainstate and a file handle.
|
||||
* @return a UniValue map containing metadata about the snapshot.
|
||||
*/
|
||||
UniValue CreateUTXOSnapshot(
|
||||
|
||||
@@ -238,6 +238,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||
{ "dumptxoutset", 1, "type", ParamFormat::STRING },
|
||||
{ "dumptxoutset", 2, "options" },
|
||||
{ "dumptxoutset", 2, "rollback", ParamFormat::JSON_OR_STRING },
|
||||
{ "dumptxoutset", 2, "in_memory" },
|
||||
{ "lockunspent", 0, "unlock" },
|
||||
{ "lockunspent", 1, "transactions" },
|
||||
{ "lockunspent", 2, "persistent" },
|
||||
|
||||
@@ -300,4 +300,26 @@ BOOST_AUTO_TEST_CASE(blockmanager_flush_block_file)
|
||||
BOOST_CHECK_EQUAL(read_block.nVersion, 2);
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(prune_lock_update_and_delete, TestingSetup)
|
||||
{
|
||||
LOCK(::cs_main);
|
||||
auto& chainman{*Assert(m_node.chainman)};
|
||||
auto& blockman{chainman.m_blockman};
|
||||
|
||||
// Create a prune lock
|
||||
blockman.UpdatePruneLock("test_lock", node::PruneLockInfo{.height_first = 100});
|
||||
|
||||
// Update it to a new height
|
||||
blockman.UpdatePruneLock("test_lock", node::PruneLockInfo{.height_first = 200});
|
||||
|
||||
// Delete existing prune lock
|
||||
BOOST_CHECK(blockman.DeletePruneLock("test_lock"));
|
||||
|
||||
// Verify deletion worked by trying to delete the same lock again
|
||||
BOOST_CHECK(!blockman.DeletePruneLock("test_lock"));
|
||||
|
||||
// Deleting a non-existent lock returns false
|
||||
BOOST_CHECK(!blockman.DeletePruneLock("nonexistent"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
@@ -19,16 +19,35 @@ class DumptxoutsetTest(BitcoinTestFramework):
|
||||
self.setup_clean_chain = True
|
||||
self.num_nodes = 1
|
||||
|
||||
def check_expected_network(self, node, active):
|
||||
rev_file = node.blocks_path / "rev00000.dat"
|
||||
bogus_file = node.blocks_path / "bogus.dat"
|
||||
rev_file.rename(bogus_file)
|
||||
assert_raises_rpc_error(
|
||||
-1, 'Could not roll back to requested height.', node.dumptxoutset, 'utxos.dat', rollback=99)
|
||||
assert_equal(node.getnetworkinfo()['networkactive'], active)
|
||||
def test_dumptxoutset_with_fork(self):
|
||||
node = self.nodes[0]
|
||||
tip = node.getbestblockhash()
|
||||
target_height = node.getblockcount() - 10
|
||||
target_hash = node.getblockhash(target_height)
|
||||
|
||||
# Create a fork of two blocks at the target height
|
||||
invalid_block = node.getblockhash(target_height + 1)
|
||||
node.invalidateblock(invalid_block)
|
||||
# Reset mocktime to not regenerate the same blockhash
|
||||
node.setmocktime(0)
|
||||
self.generate(node, 2)
|
||||
|
||||
# Move back on to actual main chain
|
||||
node.reconsiderblock(invalid_block)
|
||||
self.wait_until(lambda: node.getbestblockhash() == tip)
|
||||
|
||||
# Use dumptxoutset at the forked height
|
||||
out = node.dumptxoutset("txoutset_fork.dat", "rollback", {"rollback": target_height})
|
||||
|
||||
# Verify the snapshot was created at the target height and not the fork tip
|
||||
assert_equal(out['base_height'], target_height)
|
||||
assert_equal(out['base_hash'], target_hash)
|
||||
|
||||
# Cover the same case as above with an in-memory database
|
||||
out_mem = node.dumptxoutset("txoutset_fork_mem.dat", "rollback", {"rollback": target_height, "in_memory": True})
|
||||
assert_equal(out_mem['base_height'], target_height)
|
||||
assert_equal(out_mem['base_hash'], target_hash)
|
||||
|
||||
# Cleanup
|
||||
bogus_file.rename(rev_file)
|
||||
|
||||
def run_test(self):
|
||||
"""Test a trivial usage of the dumptxoutset RPC command."""
|
||||
@@ -71,13 +90,8 @@ class DumptxoutsetTest(BitcoinTestFramework):
|
||||
assert_raises_rpc_error(
|
||||
-8, 'Invalid snapshot type "bogus" specified. Please specify "rollback" or "latest"', node.dumptxoutset, 'utxos.dat', "bogus")
|
||||
|
||||
self.log.info("Test that dumptxoutset failure does not leave the network activity suspended when it was on previously")
|
||||
self.check_expected_network(node, True)
|
||||
|
||||
self.log.info("Test that dumptxoutset failure leaves the network activity suspended when it was off")
|
||||
node.setnetworkactive(False)
|
||||
self.check_expected_network(node, False)
|
||||
node.setnetworkactive(True)
|
||||
self.log.info("Testing dumptxoutset with chain fork at target height")
|
||||
self.test_dumptxoutset_with_fork()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -77,7 +77,7 @@ class UtxoToSqliteTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 1
|
||||
# we want to create some UTXOs with non-standard output scripts
|
||||
self.extra_args = [['-acceptnonstdtxn=1']]
|
||||
self.extra_args = [['-acceptnonstdtxn=1', '-coinstatsindex=1']]
|
||||
|
||||
def skip_test_if_missing_module(self):
|
||||
self.skip_if_no_py_sqlite3()
|
||||
@@ -143,10 +143,12 @@ class UtxoToSqliteTest(BitcoinTestFramework):
|
||||
output_direct_filename = os.path.join(self.options.tmpdir, "utxos_direct.sqlite")
|
||||
p = subprocess.Popen([sys.executable, utxo_to_sqlite_path, fifo_filename, output_direct_filename],
|
||||
stderr=subprocess.STDOUT)
|
||||
node.dumptxoutset(fifo_filename, "latest")
|
||||
target_height = node.getblockcount() - 10
|
||||
node.dumptxoutset(fifo_filename, "rollback", {"rollback": target_height})
|
||||
p.wait(timeout=10)
|
||||
muhash_direct_sqlite = calculate_muhash_from_sqlite_utxos(output_direct_filename, "hex", "hex")
|
||||
assert_equal(muhash_sqlite, muhash_direct_sqlite)
|
||||
muhash_index = node.gettxoutsetinfo('muhash', target_height)['muhash']
|
||||
assert_equal(muhash_index, muhash_direct_sqlite)
|
||||
os.remove(fifo_filename)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user