From 6ae47b58c4af904fb8c1074b670fd901ad438ace Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Thu, 10 Nov 2022 12:03:39 -0500 Subject: [PATCH 01/30] partial merge bitcoin/bitcoin#27596: validation: add ChainstateRole This is an early partial pick of bitcoin/bitcoin#27596; the remainder will be backported later. Upstream commit: c6af23c5179cc383f8e6c275373af8d11e6a989f --- src/kernel/chain.cpp | 11 +++++++++++ src/kernel/chain.h | 20 ++++++++++++++++++++ src/validation.cpp | 10 ++++++++++ src/validation.h | 7 +++++++ 4 files changed, 48 insertions(+) diff --git a/src/kernel/chain.cpp b/src/kernel/chain.cpp index 82e77125d7f3..b8037f51799e 100644 --- a/src/kernel/chain.cpp +++ b/src/kernel/chain.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -24,3 +25,13 @@ interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* index, const CBlock* data return info; } } // namespace kernel + +std::ostream& operator<<(std::ostream& os, const ChainstateRole& role) { + switch(role) { + case ChainstateRole::NORMAL: os << "normal"; break; + case ChainstateRole::ASSUMEDVALID: os << "assumedvalid"; break; + case ChainstateRole::BACKGROUND: os << "background"; break; + default: os.setstate(std::ios_base::failbit); + } + return os; +} diff --git a/src/kernel/chain.h b/src/kernel/chain.h index f0750f82663f..feba24a557e6 100644 --- a/src/kernel/chain.h +++ b/src/kernel/chain.h @@ -5,6 +5,8 @@ #ifndef BITCOIN_KERNEL_CHAIN_H #define BITCOIN_KERNEL_CHAIN_H +#include + class CBlock; class CBlockIndex; namespace interfaces { @@ -14,6 +16,24 @@ struct BlockInfo; namespace kernel { //! Return data from block index. interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* block_index, const CBlock* data = nullptr); + } // namespace kernel +//! This enum describes the various roles a specific Chainstate instance can take. +//! Other parts of the system sometimes need to vary in behavior depending on the +//! existence of a background validation chainstate, e.g. when building indexes. +enum class ChainstateRole { + // Single chainstate in use, "normal" IBD mode. + NORMAL, + + // Doing IBD-style validation in the background. Implies use of an assumed-valid + // chainstate. + BACKGROUND, + + // Active assumed-valid chainstate. Implies use of a background IBD chainstate. + ASSUMEDVALID, +}; + +std::ostream& operator<<(std::ostream& os, const ChainstateRole& role); + #endif // BITCOIN_KERNEL_CHAIN_H diff --git a/src/validation.cpp b/src/validation.cpp index 3f32df6aeca8..907e33f25f9e 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -6818,3 +6818,13 @@ bool ChainstateManager::ValidatedSnapshotCleanup() } return true; } + +ChainstateRole Chainstate::GetRole() const +{ + if (m_chainman.GetAll().size() <= 1) { + return ChainstateRole::NORMAL; + } + return (this != &m_chainman.ActiveChainstate()) ? + ChainstateRole::BACKGROUND : + ChainstateRole::ASSUMEDVALID; +} diff --git a/src/validation.h b/src/validation.h index 3e85ff3b90af..28ef3bf65afd 100644 --- a/src/validation.h +++ b/src/validation.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -539,6 +540,12 @@ class Chainstate const std::unique_ptr& chain_helper, std::optional from_snapshot_blockhash = std::nullopt); + //! Return the current role of the chainstate. See `ChainstateManager` + //! documentation for a description of the different types of chainstates. + //! + //! @sa ChainstateRole + ChainstateRole GetRole() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Return the stable EvoDB identity corresponding to this chainstate's coins DB. ::EvoDbIdentity EvoDbIdentity() const; From e1ec6f9268794ceca7fc8903b26eb03f72ca4e81 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 13:48:24 -0500 Subject: [PATCH 02/30] Merge bitcoin/bitcoin#27596: assumeutxo (2) --- contrib/devtools/test_utxo_snapshots.sh | 200 ++++++++++++ doc/design/assumeutxo.md | 2 +- doc/release-notes-27596.md | 28 ++ doc/zmq.md | 4 +- src/chain.h | 6 + src/chainparams.cpp | 29 +- src/chainparams.h | 28 +- src/index/base.cpp | 29 +- src/index/base.h | 12 +- src/init.cpp | 21 ++ src/interfaces/chain.h | 9 +- src/kernel/chain.cpp | 2 +- src/kernel/chain.h | 12 + src/net_processing.cpp | 103 +++++-- src/node/blockstorage.cpp | 254 ++++++++++++---- src/node/blockstorage.h | 114 +++++-- src/node/interfaces.cpp | 9 +- src/rpc/blockchain.cpp | 177 ++++++++++- src/test/coinstatsindex_tests.cpp | 2 +- src/test/fuzz/rpc.cpp | 2 + src/test/util/chainstate.h | 18 +- src/test/util/validation.cpp | 8 +- src/test/util/validation.h | 6 +- src/test/validation_block_tests.cpp | 2 +- .../validation_chainstatemanager_tests.cpp | 127 +++++--- src/test/validation_tests.cpp | 6 +- src/test/validationinterface_tests.cpp | 1 + src/util/vector.h | 13 + src/validation.cpp | 285 +++++++++++++----- src/validation.h | 75 +++-- src/validationinterface.cpp | 13 +- src/validationinterface.h | 19 +- src/wallet/test/fuzz/notifications.cpp | 6 +- src/wallet/wallet.cpp | 14 +- src/wallet/wallet.h | 4 +- src/zmq/zmqnotificationinterface.cpp | 6 +- src/zmq/zmqnotificationinterface.h | 2 +- test/functional/feature_assumeutxo.py | 246 +++++++++++++++ .../test_framework/test_framework.py | 4 + test/functional/test_runner.py | 1 + test/lint/lint-shell.py | 8 +- 41 files changed, 1587 insertions(+), 320 deletions(-) create mode 100755 contrib/devtools/test_utxo_snapshots.sh create mode 100644 doc/release-notes-27596.md create mode 100755 test/functional/feature_assumeutxo.py diff --git a/contrib/devtools/test_utxo_snapshots.sh b/contrib/devtools/test_utxo_snapshots.sh new file mode 100755 index 000000000000..d4c49bf098f2 --- /dev/null +++ b/contrib/devtools/test_utxo_snapshots.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# Demonstrate the creation and usage of UTXO snapshots. +# +# A server node starts up, IBDs up to a certain height, then generates a UTXO +# snapshot at that point. +# +# The server then downloads more blocks (to create a diff from the snapshot). +# +# We bring a client up, load the UTXO snapshot, and we show the client sync to +# the "network tip" and then start a background validation of the snapshot it +# loaded. We see the background validation chainstate removed after validation +# completes. +# + +export LC_ALL=C +set -e + +BASE_HEIGHT=${1:-30000} +INCREMENTAL_HEIGHT=20000 +FINAL_HEIGHT=$(($BASE_HEIGHT + $INCREMENTAL_HEIGHT)) + +SERVER_DATADIR="$(pwd)/utxodemo-data-server-$BASE_HEIGHT" +CLIENT_DATADIR="$(pwd)/utxodemo-data-client-$BASE_HEIGHT" +UTXO_DAT_FILE="$(pwd)/utxo.$BASE_HEIGHT.dat" + +# Chosen to try to not interfere with any running bitcoind processes. +SERVER_PORT=8633 +SERVER_RPC_PORT=8632 + +CLIENT_PORT=8733 +CLIENT_RPC_PORT=8732 + +SERVER_PORTS="-port=${SERVER_PORT} -rpcport=${SERVER_RPC_PORT}" +CLIENT_PORTS="-port=${CLIENT_PORT} -rpcport=${CLIENT_RPC_PORT}" + +# Ensure the client exercises all indexes to test that snapshot use works +# properly with indexes. +ALL_INDEXES="-txindex -coinstatsindex -blockfilterindex=1" + +if ! command -v jq >/dev/null ; then + echo "This script requires jq to parse JSON RPC output. Please install it." + echo "(e.g. sudo apt install jq)" + exit 1 +fi + +DUMP_OUTPUT="dumptxoutset-output-$BASE_HEIGHT.json" + +finish() { + echo + echo "Killing server and client PIDs ($SERVER_PID, $CLIENT_PID) and cleaning up datadirs" + echo + rm -f "$UTXO_DAT_FILE" "$DUMP_OUTPUT" + rm -rf "$SERVER_DATADIR" "$CLIENT_DATADIR" + kill -9 "$SERVER_PID" "$CLIENT_PID" +} + +trap finish EXIT + +# Need to specify these to trick client into accepting server as a peer +# it can IBD from, otherwise the default values prevent IBD from the server node. +EARLY_IBD_FLAGS="-maxtipage=9223372036854775207 -minimumchainwork=0x00" + +server_rpc() { + ./src/bitcoin-cli -rpcport=$SERVER_RPC_PORT -datadir="$SERVER_DATADIR" "$@" +} +client_rpc() { + ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir="$CLIENT_DATADIR" "$@" +} +server_sleep_til_boot() { + while ! server_rpc ping >/dev/null 2>&1; do sleep 0.1; done +} +client_sleep_til_boot() { + while ! client_rpc ping >/dev/null 2>&1; do sleep 0.1; done +} + +mkdir -p "$SERVER_DATADIR" "$CLIENT_DATADIR" + +echo "Hi, welcome to the assumeutxo demo/test" +echo +echo "We're going to" +echo +echo " - start up a 'server' node, sync it via mainnet IBD to height ${BASE_HEIGHT}" +echo " - create a UTXO snapshot at that height" +echo " - IBD ${INCREMENTAL_HEIGHT} more blocks on top of that" +echo +echo "then we'll demonstrate assumeutxo by " +echo +echo " - starting another node (the 'client') and loading the snapshot in" +echo " * first you'll have to modify the code slightly (chainparams) and recompile" +echo " * don't worry, we'll make it easy" +echo " - observing the client sync ${INCREMENTAL_HEIGHT} blocks on top of the snapshot from the server" +echo " - observing the client validate the snapshot chain via background IBD" +echo +read -p "Press [enter] to continue" _ + +echo +echo "-- Starting the demo. You might want to run the two following commands in" +echo " separate terminal windows:" +echo +echo " watch -n0.1 tail -n 30 $SERVER_DATADIR/debug.log" +echo " watch -n0.1 tail -n 30 $CLIENT_DATADIR/debug.log" +echo +read -p "Press [enter] to continue" _ + +echo +echo "-- IBDing the blocks (height=$BASE_HEIGHT) required to the server node..." +./src/bitcoind -logthreadnames=1 $SERVER_PORTS \ + -datadir="$SERVER_DATADIR" $EARLY_IBD_FLAGS -stopatheight="$BASE_HEIGHT" >/dev/null + +echo +echo "-- Creating snapshot at ~ height $BASE_HEIGHT ($UTXO_DAT_FILE)..." +sleep 2 +./src/bitcoind -logthreadnames=1 $SERVER_PORTS \ + -datadir="$SERVER_DATADIR" $EARLY_IBD_FLAGS -connect=0 -listen=0 >/dev/null & +SERVER_PID="$!" + +server_sleep_til_boot +server_rpc dumptxoutset "$UTXO_DAT_FILE" > "$DUMP_OUTPUT" +cat "$DUMP_OUTPUT" +kill -9 "$SERVER_PID" + +RPC_BASE_HEIGHT=$(jq -r .base_height < "$DUMP_OUTPUT") +RPC_AU=$(jq -r .txoutset_hash < "$DUMP_OUTPUT") +RPC_NCHAINTX=$(jq -r .nchaintx < "$DUMP_OUTPUT") +RPC_BLOCKHASH=$(jq -r .base_hash < "$DUMP_OUTPUT") + +# Wait for server to shutdown... +while server_rpc ping >/dev/null 2>&1; do sleep 0.1; done + +echo +echo "-- Now: add the following to CMainParams::m_assumeutxo_data" +echo " in src/kernel/chainparams.cpp, and recompile:" +echo +echo " {${RPC_BASE_HEIGHT}, AssumeutxoHash{uint256S(\"0x${RPC_AU}\")}, ${RPC_NCHAINTX}, uint256S(\"0x${RPC_BLOCKHASH}\")}," +echo +echo +echo "-- IBDing more blocks to the server node (height=$FINAL_HEIGHT) so there is a diff between snapshot and tip..." +./src/bitcoind $SERVER_PORTS -logthreadnames=1 -datadir="$SERVER_DATADIR" \ + $EARLY_IBD_FLAGS -stopatheight="$FINAL_HEIGHT" >/dev/null + +echo +echo "-- Starting the server node to provide blocks to the client node..." +./src/bitcoind $SERVER_PORTS -logthreadnames=1 -debug=net -datadir="$SERVER_DATADIR" \ + $EARLY_IBD_FLAGS -connect=0 -listen=1 >/dev/null & +SERVER_PID="$!" +server_sleep_til_boot + +echo +echo "-- Okay, what you're about to see is the client starting up and activating the snapshot." +echo " I'm going to display the top 14 log lines from the client on top of an RPC called" +echo " getchainstates, which is like getblockchaininfo but for both the snapshot and " +echo " background validation chainstates." +echo +echo " You're going to first see the snapshot chainstate sync to the server's tip, then" +echo " the background IBD chain kicks in to validate up to the base of the snapshot." +echo +echo " Once validation of the snapshot is done, you should see log lines indicating" +echo " that we've deleted the background validation chainstate." +echo +echo " Once everything completes, exit the watch command with CTRL+C." +echo +read -p "When you're ready for all this, hit [enter]" _ + +echo +echo "-- Starting the client node to get headers from the server, then load the snapshot..." +./src/bitcoind $CLIENT_PORTS $ALL_INDEXES -logthreadnames=1 -datadir="$CLIENT_DATADIR" \ + -connect=0 -addnode=127.0.0.1:$SERVER_PORT -debug=net $EARLY_IBD_FLAGS >/dev/null & +CLIENT_PID="$!" +client_sleep_til_boot + +echo +echo "-- Initial state of the client:" +client_rpc getchainstates + +echo +echo "-- Loading UTXO snapshot into client..." +client_rpc loadtxoutset "$UTXO_DAT_FILE" + +watch -n 0.3 "( tail -n 14 $CLIENT_DATADIR/debug.log ; echo ; ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat" + +echo +echo "-- Okay, now I'm going to restart the client to make sure that the snapshot chain reloads " +echo " as the main chain properly..." +echo +echo " Press CTRL+C after you're satisfied to exit the demo" +echo +read -p "Press [enter] to continue" + +while kill -0 "$CLIENT_PID"; do + sleep 1 +done +./src/bitcoind $CLIENT_PORTS $ALL_INDEXES -logthreadnames=1 -datadir="$CLIENT_DATADIR" -connect=0 \ + -addnode=127.0.0.1:$SERVER_PORT "$EARLY_IBD_FLAGS" >/dev/null & +CLIENT_PID="$!" +client_sleep_til_boot + +watch -n 0.3 "( tail -n 14 $CLIENT_DATADIR/debug.log ; echo ; ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat" + +echo +echo "-- Done!" diff --git a/doc/design/assumeutxo.md b/doc/design/assumeutxo.md index 9846f7f26a0b..b6f52845cbda 100644 --- a/doc/design/assumeutxo.md +++ b/doc/design/assumeutxo.md @@ -3,7 +3,7 @@ Assumeutxo is a feature that allows fast bootstrapping of a validating dashd instance with a very similar security model to assumevalid. -The RPC commands `dumptxoutset` and `loadtxoutset` (yet to be merged) are used to +The RPC commands `dumptxoutset` and `loadtxoutset` are used to respectively generate and load UTXO snapshots. The utility script `./contrib/devtools/utxo_snapshot.sh` may be of use. diff --git a/doc/release-notes-27596.md b/doc/release-notes-27596.md new file mode 100644 index 000000000000..799b82643fec --- /dev/null +++ b/doc/release-notes-27596.md @@ -0,0 +1,28 @@ +Pruning +------- + +When using assumeutxo with `-prune`, the prune budget may be exceeded if it is set +lower than 1100MB (i.e. `MIN_DISK_SPACE_FOR_BLOCK_FILES * 2`). Prune budget is normally +split evenly across each chainstate, unless the resulting prune budget per chainstate +is beneath `MIN_DISK_SPACE_FOR_BLOCK_FILES` in which case that value will be used. + +RPC +--- + +`loadtxoutset` has been added, which allows loading a UTXO snapshot of the format +generated by `dumptxoutset`. Once this snapshot is loaded, its contents will be +deserialized into a second chainstate data structure, which is then used to sync to +the network's tip under a security model very much like `assumevalid`. + +Meanwhile, the original chainstate will complete the initial block download process in +the background, eventually validating up to the block that the snapshot is based upon. + +The result is a usable bitcoind instance that is current with the network tip in a +matter of minutes rather than hours. UTXO snapshot are typically obtained via +third-party sources (HTTP, torrent, etc.) which is reasonable since their contents +are always checked by hash. + +You can find more information on this process in the `assumeutxo` design +document (). + +`getchainstates` has been added to aid in monitoring the assumeutxo sync process. diff --git a/doc/zmq.md b/doc/zmq.md index d9e2709b2f81..3fd715d7762d 100644 --- a/doc/zmq.md +++ b/doc/zmq.md @@ -141,11 +141,11 @@ Where the 8-byte uints correspond to the mempool sequence number. | hashtx | <32-byte transaction hash in Little Endian> | -`rawblock`: Notifies when the chain tip is updated. Messages are ZMQ multipart messages with three parts. The first part is the topic (`rawblock`), the second part is the serialized block, and the last part is a sequence number (representing the message count to detect lost messages). +`rawblock`: Notifies when the chain tip is updated. When assumeutxo is in use, this notification will not be issued for historical blocks connected to the background validation chainstate. Messages are ZMQ multipart messages with three parts. The first part is the topic (`rawblock`), the second part is the serialized block, and the last part is a sequence number (representing the message count to detect lost messages). | rawblock | | -`hashblock`: Notifies when the chain tip is updated. Messages are ZMQ multipart messages with three parts. The first part is the topic (`hashblock`), the second part is the 32-byte block hash, and the last part is a sequence number (representing the message count to detect lost messages). +`hashblock`: Notifies when the chain tip is updated. When assumeutxo is in use, this notification will not be issued for historical blocks connected to the background validation chainstate. Messages are ZMQ multipart messages with three parts. The first part is the topic (`hashblock`), the second part is the 32-byte block hash, and the last part is a sequence number (representing the message count to detect lost messages). | hashblock | <32-byte block hash in Little Endian> | diff --git a/src/chain.h b/src/chain.h index f7f1d24fb8a4..d8926c743728 100644 --- a/src/chain.h +++ b/src/chain.h @@ -261,6 +261,12 @@ class CBlockIndex * * Does not imply the transactions are consensus-valid (ConnectTip might fail) * Does not imply the transactions are still stored on disk. (IsBlockPruned might return true) + * + * Note that this will be true for the snapshot base block, if one is loaded (and + * all subsequent assumed-valid blocks) since its nChainTx value will have been set + * manually based on the related AssumeutxoData entry. + * + * TODO: potentially change the name of this based on the fact above. */ bool HaveTxsDownloaded() const { return nChainTx != 0; } diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 565d2486bd97..24228164421c 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -339,7 +339,7 @@ class CMainParams : public CChainParams { } }; - m_assumeutxo_data = MapAssumeutxo{ + m_assumeutxo_data = { // TODO to be specified in a future patch. }; @@ -514,7 +514,7 @@ class CTestNetParams : public CChainParams { } }; - m_assumeutxo_data = MapAssumeutxo{ + m_assumeutxo_data = { // TODO to be specified in a future patch. }; @@ -879,14 +879,29 @@ class CRegTestParams : public CChainParams { } }; - m_assumeutxo_data = MapAssumeutxo{ + m_assumeutxo_data = { { - 110, - {AssumeutxoHash{uint256S("0x9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b")}, EvoSnapshotHash{uint256{}}, 110}, + .height = 110, + .hash_serialized = AssumeutxoHash{uint256S("0x9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b")}, + .evo_hash = EvoSnapshotHash{uint256{}}, + .nChainTx = 110, + .blockhash = uint256{}, }, { - 200, - {AssumeutxoHash{uint256S("0x8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3")}, EvoSnapshotHash{uint256{}}, 200}, + .height = 200, + .hash_serialized = AssumeutxoHash{uint256S("0x8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3")}, + .evo_hash = EvoSnapshotHash{uint256{}}, + .nChainTx = 200, + .blockhash = uint256{}, + }, + { + // For use by test/functional/feature_assumeutxo.py. Dash-specific + // hashes are filled in by the test adaptation follow-up. + .height = 299, + .hash_serialized = AssumeutxoHash{uint256{}}, + .evo_hash = EvoSnapshotHash{uint256{}}, + .nChainTx = 300, + .blockhash = uint256{}, }, }; diff --git a/src/chainparams.h b/src/chainparams.h index fa974ba40cb7..e5e6a38e2a73 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -44,20 +45,24 @@ struct EvoSnapshotHash : public BaseHash { * as valid. */ struct AssumeutxoData { + int height; + //! The expected hash of the deserialized UTXO set. - const AssumeutxoHash hash_serialized; + AssumeutxoHash hash_serialized; //! The expected single-SHA256 hash of the canonical Dash evo section. - const EvoSnapshotHash evo_hash; + EvoSnapshotHash evo_hash; //! Used to populate the nChainTx value, which is used during BlockManager::LoadBlockIndex(). //! //! We need to hardcode the value here because this is computed cumulatively using block data, //! which we do not necessarily have at the time of snapshot load. - const unsigned int nChainTx; -}; + unsigned int nChainTx; -using MapAssumeutxo = std::map; + //! The hash of the base block for this snapshot. Used to refer to assumeutxo data + //! prior to having a loaded blockindex. + uint256 blockhash; +}; /** * Holds various statistics on transactions within a chain. Used to estimate @@ -137,9 +142,14 @@ class CChainParams const std::vector& FixedSeeds() const { return vFixedSeeds; } const CCheckpointData& Checkpoints() const { return checkpointData; } - //! Get allowed assumeutxo configuration. - //! @see ChainstateManager - const MapAssumeutxo& Assumeutxo() const { return m_assumeutxo_data; } + std::optional AssumeutxoForHeight(int height) const + { + return FindFirst(m_assumeutxo_data, [&](const auto& d) { return d.height == height; }); + } + std::optional AssumeutxoForBlockhash(const uint256& blockhash) const + { + return FindFirst(m_assumeutxo_data, [&](const auto& d) { return d.blockhash == blockhash; }); + } const ChainTxData& TxData() const { return chainTxData; } void UpdateDIP3Parameters(int nActivationHeight, int nEnforcementHeight); @@ -186,7 +196,7 @@ class CChainParams bool m_is_mockable_chain; int nLLMQConnectionRetryTimeout; CCheckpointData checkpointData; - MapAssumeutxo m_assumeutxo_data; + std::vector m_assumeutxo_data; ChainTxData chainTxData; int nPoolMinParticipants; int nPoolMaxParticipants; diff --git a/src/index/base.cpp b/src/index/base.cpp index b52ff2d11055..dfad899cc180 100644 --- a/src/index/base.cpp +++ b/src/index/base.cpp @@ -78,7 +78,8 @@ bool BaseIndex::Init() } LOCK(cs_main); - CChain& active_chain = m_chainstate->m_chain; + CChain& index_chain = m_chainstate->m_chain; + if (locator.IsNull()) { SetBestBlockIndex(nullptr); } else { @@ -152,6 +153,8 @@ void BaseIndex::ThreadSync() std::chrono::steady_clock::time_point last_locator_write_time{0s}; while (true) { if (m_interrupt) { + LogPrintf("%s: m_interrupt set; exiting ThreadSync\n", GetName()); + SetBestBlockIndex(pindex); // No need to handle errors in Commit. If it fails, the error will be already be // logged. The best way to recover is to continue, as index cannot be corrupted by @@ -259,8 +262,19 @@ bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_ti return true; } -void BaseIndex::BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) +void BaseIndex::BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) { + // Ignore events from the assumed-valid chain; we will process its blocks + // (sequentially) after it is fully verified by the background chainstate. This + // is to avoid any out-of-order indexing. + // + // TODO at some point we could parameterize whether a particular index can be + // built out of order, but for now just do the conservative simple thing. + if (role == ChainstateRole::ASSUMEDVALID) { + return; + } + + // Ignore BlockConnected signals until we have fully indexed the chain. if (!m_synced) { return; } @@ -329,8 +343,14 @@ void BaseIndex::BlockDisconnected(const std::shared_ptr& block, co } } -void BaseIndex::ChainStateFlushed(const CBlockLocator& locator) +void BaseIndex::ChainStateFlushed(ChainstateRole role, const CBlockLocator& locator) { + // Ignore events from the assumed-valid chain; we will process its blocks + // (sequentially) after it is fully verified by the background chainstate. + if (role == ChainstateRole::ASSUMEDVALID) { + return; + } + if (!m_synced) { return; } @@ -401,7 +421,8 @@ bool BaseIndex::Start() { // m_chainstate member gives indexing code access to node internals. It is // removed in followup https://github.com/bitcoin/bitcoin/pull/24230 - m_chainstate = &m_chain->context()->chainman->ActiveChainstate(); + m_chainstate = &WITH_LOCK(::cs_main, return m_chain->context()->chainman->GetChainstateForIndexing()); + m_interrupt.reset(); // Need to register this ValidationInterface before running Init(), so that // callbacks are not missed if Init sets m_synced to true. RegisterValidationInterface(this); diff --git a/src/index/base.h b/src/index/base.h index daf411c8053b..ad84b8e67ec6 100644 --- a/src/index/base.h +++ b/src/index/base.h @@ -31,6 +31,11 @@ struct IndexSummary { * Base class for indices of blockchain data. This implements * CValidationInterface and ensures blocks are indexed sequentially according * to their position in the active chain. + * + * In the presence of multiple chainstates (i.e. if a UTXO snapshot is loaded), + * only the background "IBD" chainstate will be indexed to avoid building the + * index out of order. When the background chainstate completes validation, the + * index will be reinitialized and indexing will continue. */ class BaseIndex : public CValidationInterface { @@ -101,11 +106,11 @@ class BaseIndex : public CValidationInterface Chainstate* m_chainstate{nullptr}; const std::string m_name; - void BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) override; + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) override; void BlockDisconnected(const std::shared_ptr& block, const CBlockIndex* pindex) override; - void ChainStateFlushed(const CBlockLocator& locator) override; + void ChainStateFlushed(ChainstateRole role, const CBlockLocator& locator) override; /// Initialize internal state from the database and block index. [[nodiscard]] virtual bool CustomInit(const std::optional& block) { return true; } @@ -143,6 +148,9 @@ class BaseIndex : public CValidationInterface /// Destructor interrupts sync thread if running and blocks until it exits. virtual ~BaseIndex(); + /// Get the name of the index for display in logs. + const std::string& GetName() const LIFETIMEBOUND { return m_name; } + /// Blocks the current thread until the index is caught up to the current /// state of the block chain. This only blocks if the index has gotten in /// sync once and only needs to process blocks in the ValidationInterface diff --git a/src/init.cpp b/src/init.cpp index d083d168270f..b17b6f1c09c7 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1979,6 +1979,21 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) node.chainman = std::make_unique(chainman_opts); ChainstateManager& chainman = *node.chainman; + // This is defined here instead of validation.h to avoid a dependency on + // index/base from libdashconsensus-facing validation code. + chainman.restart_indexes = [&node]() { + LogPrintf("[snapshot] restarting indexes\n"); + SyncWithValidationInterfaceQueue(); + + for (auto* index : node.indexes) { + index->Interrupt(); + index->Stop(); + if (!index->Start()) { + LogPrintf("[snapshot] WARNING failed to restart index %s on snapshot chain\n", index->GetName()); + } + } + }; + /** * The manager needs to be constructed regardless of whether governance * validation is needed or not. @@ -2213,6 +2228,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (!g_txindex->Start()) { return false; } + node.indexes.push_back(g_txindex.get()); } if (args.GetBoolArg("-addressindex", DEFAULT_ADDRESSINDEX)) { @@ -2220,6 +2236,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (!node.address_index->Start()) { return false; } + node.indexes.push_back(g_addressindex.get()); } if (args.GetBoolArg("-timestampindex", DEFAULT_TIMESTAMPINDEX)) { @@ -2227,6 +2244,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (!node.timestamp_index->Start()) { return false; } + node.indexes.push_back(g_timestampindex.get()); } if (args.GetBoolArg("-spentindex", DEFAULT_SPENTINDEX)) { @@ -2234,6 +2252,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (!node.spent_index->Start()) { return false; } + node.indexes.push_back(g_spentindex.get()); } for (const auto& filter_type : g_enabled_filter_types) { @@ -2241,6 +2260,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (!GetBlockFilterIndex(filter_type)->Start()) { return false; } + node.indexes.push_back(GetBlockFilterIndex(filter_type)); } if (args.GetBoolArg("-coinstatsindex", DEFAULT_COINSTATSINDEX)) { @@ -2248,6 +2268,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (!g_coin_stats_index->Start()) { return false; } + node.indexes.push_back(g_coin_stats_index.get()); } // ********************************************************* Step 9: load wallet diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index de2ce3bd3bb3..260d053c23f8 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -28,6 +28,8 @@ class CBlockIndex; class Coin; class uint256; enum class MemPoolRemovalReason; +enum class RBFTransactionState; +enum class ChainstateRole; struct bilingual_str; struct CBlockLocator; struct FeeCalculation; @@ -96,6 +98,9 @@ struct BlockInfo { unsigned data_pos = 0; const CBlock* data = nullptr; const CBlockUndo* undo_data = nullptr; + // The maximum time in the chain up to and including this block. + // A timestamp that can only move forward. + unsigned int chain_time_max{0}; BlockInfo(const uint256& hash LIFETIMEBOUND) : hash(hash) {} }; @@ -302,10 +307,10 @@ class Chain virtual ~Notifications() {} virtual void transactionAddedToMempool(const CTransactionRef& tx, int64_t nAcceptTime) {} virtual void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) {} - virtual void blockConnected(const BlockInfo& block) {} + virtual void blockConnected(ChainstateRole role, const BlockInfo& block) {} virtual void blockDisconnected(const BlockInfo& block) {} virtual void updatedBlockTip() {} - virtual void chainStateFlushed(const CBlockLocator& locator) {} + virtual void chainStateFlushed(ChainstateRole role, const CBlockLocator& locator) {} virtual void notifyChainLock(const CBlockIndex* pindexChainLock, const std::shared_ptr& clsig) {} virtual void notifyTransactionLock(const CTransactionRef &tx, const std::shared_ptr& islock) {} }; diff --git a/src/kernel/chain.cpp b/src/kernel/chain.cpp index b8037f51799e..61cea51bd92b 100644 --- a/src/kernel/chain.cpp +++ b/src/kernel/chain.cpp @@ -9,7 +9,6 @@ #include class CBlock; - namespace kernel { interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* index, const CBlock* data) { @@ -17,6 +16,7 @@ interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* index, const CBlock* data if (index) { info.prev_hash = index->pprev ? index->pprev->phashBlock : nullptr; info.height = index->nHeight; + info.chain_time_max = index->GetBlockTimeMax(); LOCK(::cs_main); info.file_number = index->nFile; info.data_pos = index->nDataPos; diff --git a/src/kernel/chain.h b/src/kernel/chain.h index feba24a557e6..d499af333444 100644 --- a/src/kernel/chain.h +++ b/src/kernel/chain.h @@ -19,6 +19,18 @@ interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* block_index, const CBlock } // namespace kernel +class CBlock; +class CBlockIndex; +namespace interfaces { +struct BlockInfo; +} // namespace interfaces + +namespace kernel { +//! Return data from block index. +interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* block_index, const CBlock* data = nullptr); + +} // namespace kernel + //! This enum describes the various roles a specific Chainstate instance can take. //! Other parts of the system sometimes need to vary in behavior depending on the //! existence of a background validation chainstate, e.g. when building indexes. diff --git a/src/net_processing.cpp b/src/net_processing.cpp index e25a9a1230c5..8bf2addfc162 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include #include @@ -581,7 +583,7 @@ class PeerManagerImpl final : public PeerManager } /** Overridden from CValidationInterface. */ - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !m_recent_confirmed_transactions_mutex); void BlockDisconnected(const std::shared_ptr &block, const CBlockIndex* pindex) override EXCLUSIVE_LOCKS_REQUIRED(!m_recent_confirmed_transactions_mutex); @@ -1049,6 +1051,11 @@ class PeerManagerImpl final : public PeerManager */ void FindNextBlocksToDownload(const Peer& peer, unsigned int count, std::vector& vBlocks, NodeId& nodeStaller) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + /** Request blocks for the background chainstate, if one is in use. */ + void TryDownloadingHistoricalBlocks(const Peer& peer, unsigned int count, std::vector& vBlocks, const CBlockIndex* from_tip, const CBlockIndex* target_block) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + + void FindNextBlocks(std::vector& vBlocks, const Peer& peer, CNodeState* state, const CBlockIndex* pindexWalk, unsigned int count, int nWindowEnd, const CChain* activeChain=nullptr, NodeId* nodeStaller=nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + /* Multimap used to preserve insertion order */ typedef std::multimap::iterator>> BlockDownloadMap; BlockDownloadMap mapBlocksInFlight GUARDED_BY(cs_main); @@ -1443,6 +1450,7 @@ void PeerManagerImpl::UpdateBlockAvailability(NodeId nodeid, const uint256 &hash } } +// Logic for calculating which blocks to download from a given peer, given our current tip. void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int count, std::vector& vBlocks, NodeId& nodeStaller) { if (count == 0) @@ -1472,12 +1480,47 @@ void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int co if (state->pindexLastCommonBlock == state->pindexBestKnownBlock) return; - std::vector vToFetch; const CBlockIndex *pindexWalk = state->pindexLastCommonBlock; // Never fetch further than the best block we know the peer has, or more than BLOCK_DOWNLOAD_WINDOW + 1 beyond the last // linked block we have in common with this peer. The +1 is so we can detect stalling, namely if we would be able to // download that next block if the window were 1 larger. int nWindowEnd = state->pindexLastCommonBlock->nHeight + BLOCK_DOWNLOAD_WINDOW; + + FindNextBlocks(vBlocks, peer, state, pindexWalk, count, nWindowEnd, &m_chainman.ActiveChain(), &nodeStaller); +} + +void PeerManagerImpl::TryDownloadingHistoricalBlocks(const Peer& peer, unsigned int count, std::vector& vBlocks, const CBlockIndex *from_tip, const CBlockIndex* target_block) +{ + Assert(from_tip); + Assert(target_block); + + if (vBlocks.size() >= count) { + return; + } + + vBlocks.reserve(count); + CNodeState *state = Assert(State(peer.m_id)); + + if (state->pindexBestKnownBlock == nullptr || state->pindexBestKnownBlock->GetAncestor(target_block->nHeight) != target_block) { + // This peer can't provide us the complete series of blocks leading up to the + // assumeutxo snapshot base. + // + // Presumably this peer's chain has less work than our ActiveChain()'s tip, or else we + // will eventually crash when we try to reorg to it. Let other logic + // deal with whether we disconnect this peer. + // + // TODO at some point in the future, we might choose to request what blocks + // this peer does have from the historical chain, despite it not having a + // complete history beneath the snapshot base. + return; + } + + FindNextBlocks(vBlocks, peer, state, from_tip, count, std::min(from_tip->nHeight + BLOCK_DOWNLOAD_WINDOW, target_block->nHeight)); +} + +void PeerManagerImpl::FindNextBlocks(std::vector& vBlocks, const Peer& peer, CNodeState *state, const CBlockIndex *pindexWalk, unsigned int count, int nWindowEnd, const CChain* activeChain, NodeId* nodeStaller) +{ + std::vector vToFetch; int nMaxHeight = std::min(state->pindexBestKnownBlock->nHeight, nWindowEnd + 1); NodeId waitingfor = -1; while (pindexWalk->nHeight < nMaxHeight) { @@ -1501,8 +1544,8 @@ void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int co // We consider the chain that this peer is on invalid. return; } - if (pindex->nStatus & BLOCK_HAVE_DATA || m_chainman.ActiveChain().Contains(pindex)) { - if (pindex->HaveTxsDownloaded()) + if (pindex->nStatus & BLOCK_HAVE_DATA || (activeChain && activeChain->Contains(pindex))) { + if (activeChain && pindex->HaveTxsDownloaded()) state->pindexLastCommonBlock = pindex; } else if (!IsBlockRequested(pindex->GetBlockHash())) { // The block is not already downloaded, and not yet in flight. @@ -1510,7 +1553,7 @@ void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int co // We reached the end of the window. if (vBlocks.size() == 0 && waitingfor != peer.m_id) { // We aren't able to fetch anything, but we would be if the download window was one larger. - nodeStaller = waitingfor; + if (nodeStaller) *nodeStaller = waitingfor; } return; } @@ -2105,8 +2148,29 @@ void PeerManagerImpl::StartScheduledTasks(CScheduler& scheduler) * block. Also save the time of the last tip update and * possibly reduce dynamic block stalling timeout. */ -void PeerManagerImpl::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) +void PeerManagerImpl::BlockConnected( + ChainstateRole role, + const std::shared_ptr& pblock, + const CBlockIndex* pindex) { + // Update this for all chainstate roles so that we don't mistakenly see peers + // helping us do background IBD as having a stale tip. + m_last_tip_update = GetTime(); + + // In case the dynamic timeout was doubled once or more, reduce it slowly back to its default value + auto stalling_timeout = m_block_stalling_timeout.load(); + Assume(stalling_timeout >= BLOCK_STALLING_TIMEOUT_DEFAULT); + if (stalling_timeout != BLOCK_STALLING_TIMEOUT_DEFAULT) { + const auto new_timeout = std::max(std::chrono::duration_cast(stalling_timeout * 0.85), BLOCK_STALLING_TIMEOUT_DEFAULT); + if (m_block_stalling_timeout.compare_exchange_strong(stalling_timeout, new_timeout)) { + LogPrint(BCLog::NET, "Decreased stalling timeout to %d seconds\n", count_seconds(new_timeout)); + } + } + + if (role == ChainstateRole::BACKGROUND) { + return; + } + // Orphans included in or conflicted by the block can never be accepted, so drop them before // reconsidering the ones the block may have just made acceptable. m_orphanage.EraseForBlock(*pblock); @@ -2126,24 +2190,12 @@ void PeerManagerImpl::BlockConnected(const std::shared_ptr& pblock } } - m_last_tip_update = GetTime(); - { LOCK(m_recent_confirmed_transactions_mutex); for (const auto& ptx : pblock->vtx) { m_recent_confirmed_transactions.insert(ptx->GetHash()); } } - - // In case the dynamic timeout was doubled once or more, reduce it slowly back to its default value - auto stalling_timeout = m_block_stalling_timeout.load(); - Assume(stalling_timeout >= BLOCK_STALLING_TIMEOUT_DEFAULT); - if (stalling_timeout != BLOCK_STALLING_TIMEOUT_DEFAULT) { - const auto new_timeout = std::max(std::chrono::duration_cast(stalling_timeout * 0.85), BLOCK_STALLING_TIMEOUT_DEFAULT); - if (m_block_stalling_timeout.compare_exchange_strong(stalling_timeout, new_timeout)) { - LogPrint(BCLog::NET, "Decreased stalling timeout to %d seconds\n", count_seconds(new_timeout)); - } - } } void PeerManagerImpl::BlockDisconnected(const std::shared_ptr &block, const CBlockIndex* pindex) @@ -6688,7 +6740,20 @@ bool PeerManagerImpl::SendMessages(CNode* pto) if (CanServeBlocks(*peer) && pto->CanRelay() && ((sync_blocks_and_headers_from_peer && !IsLimitedPeer(*peer)) || !m_chainman.ActiveChainstate().IsInitialBlockDownload()) && state.vBlocksInFlight.size() < MAX_BLOCKS_IN_TRANSIT_PER_PEER) { std::vector vToDownload; NodeId staller = -1; - FindNextBlocksToDownload(*peer, MAX_BLOCKS_IN_TRANSIT_PER_PEER - state.vBlocksInFlight.size(), vToDownload, staller); + auto get_inflight_budget = [&state]() { + return std::max(0, MAX_BLOCKS_IN_TRANSIT_PER_PEER - static_cast(state.vBlocksInFlight.size())); + }; + + // If a snapshot chainstate is in use, we want to find its next blocks + // before the background chainstate to prioritize getting to network tip. + FindNextBlocksToDownload(*peer, get_inflight_budget(), vToDownload, staller); + if (m_chainman.BackgroundSyncInProgress() && !IsLimitedPeer(*peer)) { + TryDownloadingHistoricalBlocks( + *peer, + get_inflight_budget(), + vToDownload, m_chainman.GetBackgroundSyncTip(), + Assert(m_chainman.GetSnapshotBaseBlock())); + } for (const CBlockIndex *pindex : vToDownload) { vGetData.emplace_back(MSG_BLOCK, pindex->GetBlockHash()); BlockRequested(pto->GetId(), *pindex); diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 045b665731be..96bbee3c09ed 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include #include @@ -162,40 +164,60 @@ void BlockManager::PruneOneBlockFile(const int fileNumber) m_dirty_fileinfo.insert(fileNumber); } -void BlockManager::FindFilesToPruneManual(std::set& setFilesToPrune, int nManualPruneHeight, int chain_tip_height) +void BlockManager::FindFilesToPruneManual( + std::set& setFilesToPrune, + int nManualPruneHeight, + const Chainstate& chain, + ChainstateManager& chainman) { assert(fPruneMode && nManualPruneHeight > 0); LOCK2(cs_main, cs_LastBlockFile); - if (chain_tip_height < 0) { + if (chain.m_chain.Height() < 0) { return; } - // last block to prune is the lesser of (user-specified height, MIN_BLOCKS_TO_KEEP from the tip) - unsigned int nLastBlockWeCanPrune = std::min((unsigned)nManualPruneHeight, chain_tip_height - MIN_BLOCKS_TO_KEEP); + const auto [min_block_to_prune, last_block_can_prune] = chainman.GetPruneRange(chain, nManualPruneHeight); + int count = 0; - for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) { - if (m_blockfile_info[fileNumber].nSize == 0 || m_blockfile_info[fileNumber].nHeightLast > nLastBlockWeCanPrune) { + for (int fileNumber = 0; fileNumber < this->MaxBlockfileNum(); fileNumber++) { + const auto& fileinfo = m_blockfile_info[fileNumber]; + if (fileinfo.nSize == 0 || fileinfo.nHeightLast > (unsigned)last_block_can_prune || fileinfo.nHeightFirst < (unsigned)min_block_to_prune) { continue; } + PruneOneBlockFile(fileNumber); setFilesToPrune.insert(fileNumber); count++; } - LogPrintf("Prune (Manual): prune_height=%d removed %d blk/rev pairs\n", nLastBlockWeCanPrune, count); + LogPrintf("[%s] Prune (Manual): prune_height=%d removed %d blk/rev pairs\n", + chain.GetRole(), last_block_can_prune, count); } -void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPruneAfterHeight, int chain_tip_height, int prune_height, bool is_ibd) +void BlockManager::FindFilesToPrune( + std::set& setFilesToPrune, + int last_prune, + const Chainstate& chain, + ChainstateManager& chainman) { LOCK2(cs_main, cs_LastBlockFile); - if (chain_tip_height < 0 || nPruneTarget == 0) { + if (chain.m_chain.Height() < 0 || nPruneTarget == 0) { + return; + } + + // Distribute our -prune budget over all chainstates. + const auto target = std::max( + MIN_DISK_SPACE_FOR_BLOCK_FILES, nPruneTarget / chainman.GetAll().size()); + + if (target == 0) { return; } - if ((uint64_t)chain_tip_height <= nPruneAfterHeight) { + if (static_cast(chain.m_chain.Height()) <= chainman.GetParams().PruneAfterHeight()) { return; } - unsigned int nLastBlockWeCanPrune{(unsigned)std::min(prune_height, chain_tip_height - static_cast(MIN_BLOCKS_TO_KEEP))}; + const auto [min_block_to_prune, last_block_can_prune] = chainman.GetPruneRange(chain, last_prune); + uint64_t nCurrentUsage = CalculateCurrentUsage(); // We don't check to prune until after we've allocated new space for files // So we should leave a buffer under our target to account for another allocation @@ -204,29 +226,31 @@ void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPr uint64_t nBytesToPrune; int count = 0; - if (nCurrentUsage + nBuffer >= nPruneTarget) { + if (nCurrentUsage + nBuffer >= target) { // On a prune event, the chainstate DB is flushed. // To avoid excessive prune events negating the benefit of high dbcache // values, we should not prune too rapidly. // So when pruning in IBD, increase the buffer a bit to avoid a re-prune too soon. - if (is_ibd) { + if (chainman.IsInitialBlockDownload()) { // Since this is only relevant during IBD, we use a fixed 10% - nBuffer += nPruneTarget / 10; + nBuffer += target / 10; } - for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) { - nBytesToPrune = m_blockfile_info[fileNumber].nSize + m_blockfile_info[fileNumber].nUndoSize; + for (int fileNumber = 0; fileNumber < this->MaxBlockfileNum(); fileNumber++) { + const auto& fileinfo = m_blockfile_info[fileNumber]; + nBytesToPrune = fileinfo.nSize + fileinfo.nUndoSize; - if (m_blockfile_info[fileNumber].nSize == 0) { + if (fileinfo.nSize == 0) { continue; } - if (nCurrentUsage + nBuffer < nPruneTarget) { // are we below our target? + if (nCurrentUsage + nBuffer < target) { // are we below our target? break; } - // don't prune files that could have a block within MIN_BLOCKS_TO_KEEP of the main chain's tip but keep scanning - if (m_blockfile_info[fileNumber].nHeightLast > nLastBlockWeCanPrune) { + // don't prune files that could have a block that's not within the allowable + // prune range for the chain being pruned. + if (fileinfo.nHeightLast > (unsigned)last_block_can_prune || fileinfo.nHeightFirst < (unsigned)min_block_to_prune) { continue; } @@ -238,10 +262,10 @@ void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPr } } - LogPrint(BCLog::PRUNE, "target=%dMiB actual=%dMiB diff=%dMiB max_prune_height=%d removed %d blk/rev pairs\n", - nPruneTarget/1024/1024, nCurrentUsage/1024/1024, - ((int64_t)nPruneTarget - (int64_t)nCurrentUsage)/1024/1024, - nLastBlockWeCanPrune, count); + LogPrint(BCLog::PRUNE, "[%s] target=%dMiB actual=%dMiB diff=%dMiB min_height=%d max_prune_height=%d removed %d blk/rev pairs\n", + chain.GetRole(), target / 1024 / 1024, nCurrentUsage / 1024 / 1024, + (int64_t(target) - int64_t(nCurrentUsage)) / 1024 / 1024, + min_block_to_prune, last_block_can_prune, count); } void BlockManager::UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info) { @@ -271,7 +295,7 @@ CBlockIndex* BlockManager::InsertBlockIndex(const uint256& hash) return pindex; } -bool BlockManager::LoadBlockIndex() +bool BlockManager::LoadBlockIndex(const std::optional& snapshot_blockhash) { // Snapshot promotion reloads the block index a second time in the same // process (see node::LoadChainstate), and both containers below are rebuilt @@ -292,6 +316,25 @@ bool BlockManager::LoadBlockIndex() } } + if (snapshot_blockhash) { + const AssumeutxoData au_data = *Assert(GetParams().AssumeutxoForBlockhash(*snapshot_blockhash)); + m_snapshot_height = au_data.height; + CBlockIndex* base{LookupBlockIndex(*snapshot_blockhash)}; + + // Since nChainTx (responsible for estimated progress) isn't persisted + // to disk, we must bootstrap the value for assumedvalid chainstates + // from the hardcoded assumeutxo chainparams. + base->nChainTx = au_data.nChainTx; + LogPrintf("[snapshot] set nChainTx=%d for %s\n", au_data.nChainTx, snapshot_blockhash->ToString()); + } else { + // If this isn't called with a snapshot blockhash, make sure the cached snapshot height + // is null. This is relevant during snapshot completion, when the blockman may be loaded + // with a height that then needs to be cleared after the snapshot is fully validated. + m_snapshot_height.reset(); + } + + Assert(m_snapshot_height.has_value() == snapshot_blockhash.has_value()); + // Calculate nChainWork std::vector vSortedByHeight{GetAllBlockIndices()}; std::sort(vSortedByHeight.begin(), vSortedByHeight.end(), @@ -313,7 +356,11 @@ bool BlockManager::LoadBlockIndex() // Pruned nodes may have deleted the block. if (pindex->nTx > 0) { if (pindex->pprev) { - if (pindex->pprev->nChainTx > 0) { + if (m_snapshot_height && pindex->nHeight == *m_snapshot_height && + pindex->GetBlockHash() == *snapshot_blockhash) { + // Should have been set above; don't disturb it with code below. + Assert(pindex->nChainTx > 0); + } else if (pindex->pprev->nChainTx > 0) { pindex->nChainTx = pindex->pprev->nChainTx + pindex->nTx; } else { pindex->nChainTx = 0; @@ -350,27 +397,29 @@ bool BlockManager::WriteBlockIndexDB() vBlocks.push_back(*it); m_dirty_blockindex.erase(it++); } - if (!m_block_tree_db->WriteBatchSync(vFiles, m_last_blockfile, vBlocks)) { + int max_blockfile = WITH_LOCK(cs_LastBlockFile, return this->MaxBlockfileNum()); + if (!m_block_tree_db->WriteBatchSync(vFiles, max_blockfile, vBlocks)) { return false; } return true; } -bool BlockManager::LoadBlockIndexDB() +bool BlockManager::LoadBlockIndexDB(const std::optional& snapshot_blockhash) { - if (!LoadBlockIndex()) { + if (!LoadBlockIndex(snapshot_blockhash)) { return false; } + int max_blockfile_num{0}; // Load block file info - m_block_tree_db->ReadLastBlockFile(m_last_blockfile); - m_blockfile_info.resize(m_last_blockfile + 1); - LogPrintf("%s: last block file = %i\n", __func__, m_last_blockfile); - for (int nFile = 0; nFile <= m_last_blockfile; nFile++) { + m_block_tree_db->ReadLastBlockFile(max_blockfile_num); + m_blockfile_info.resize(max_blockfile_num + 1); + LogPrintf("%s: last block file = %i\n", __func__, max_blockfile_num); + for (int nFile = 0; nFile <= max_blockfile_num; nFile++) { m_block_tree_db->ReadBlockFileInfo(nFile, m_blockfile_info[nFile]); } - LogPrintf("%s: last block file info: %s\n", __func__, m_blockfile_info[m_last_blockfile].ToString()); - for (int nFile = m_last_blockfile + 1; true; nFile++) { + LogPrintf("%s: last block file info: %s\n", __func__, m_blockfile_info[max_blockfile_num].ToString()); + for (int nFile = max_blockfile_num + 1; true; nFile++) { CBlockFileInfo info; if (m_block_tree_db->ReadBlockFileInfo(nFile, info)) { m_blockfile_info.push_back(info); @@ -394,6 +443,15 @@ bool BlockManager::LoadBlockIndexDB() } } + { + // Initialize the blockfile cursors. + LOCK(cs_LastBlockFile); + for (size_t i = 0; i < m_blockfile_info.size(); ++i) { + const auto last_height_in_file = m_blockfile_info[i].nHeightLast; + m_blockfile_cursors[BlockfileTypeForHeight(last_height_in_file)] = {static_cast(i), 0}; + } + } + // Check whether we have ever pruned block & undo files m_block_tree_db->ReadFlag("prunedblockfiles", m_have_pruned); if (m_have_pruned) { @@ -417,12 +475,13 @@ bool BlockManager::LoadBlockIndexDB() void BlockManager::ScanAndUnlinkAlreadyPrunedFiles() { AssertLockHeld(::cs_main); + int max_blockfile = WITH_LOCK(cs_LastBlockFile, return this->MaxBlockfileNum()); if (!m_have_pruned) { return; } std::set block_files_to_prune; - for (int file_number = 0; file_number < m_last_blockfile; file_number++) { + for (int file_number = 0; file_number < max_blockfile; file_number++) { if (m_blockfile_info[file_number].nSize == 0) { block_files_to_prune.insert(file_number); } @@ -573,16 +632,19 @@ bool UndoReadFromDisk(CBlockUndo& blockundo, const CBlockIndex* pindex) return true; } -void BlockManager::FlushUndoFile(int block_file, bool finalize) +bool BlockManager::FlushUndoFile(int block_file, bool finalize) { FlatFilePos undo_pos_old(block_file, m_blockfile_info[block_file].nUndoSize); if (!UndoFileSeq().Flush(undo_pos_old, finalize)) { AbortNode("Flushing undo file to disk failed. This is likely the result of an I/O error."); + return false; } + return true; } -void BlockManager::FlushBlockFile(bool fFinalize, bool finalize_undo) +bool BlockManager::FlushBlockFile(int blockfile_num, bool fFinalize, bool finalize_undo) { + bool success{true}; LOCK(cs_LastBlockFile); if (m_blockfile_info.size() < 1) { @@ -590,17 +652,43 @@ void BlockManager::FlushBlockFile(bool fFinalize, bool finalize_undo) // chainstate init, when we call ChainstateManager::MaybeRebalanceCaches() (which // then calls FlushStateToDisk()), resulting in a call to this function before we // have populated `m_blockfile_info` via LoadBlockIndexDB(). - return; + return true; } - assert(static_cast(m_blockfile_info.size()) > m_last_blockfile); + assert(static_cast(m_blockfile_info.size()) > blockfile_num); - FlatFilePos block_pos_old(m_last_blockfile, m_blockfile_info[m_last_blockfile].nSize); + FlatFilePos block_pos_old(blockfile_num, m_blockfile_info[blockfile_num].nSize); if (!BlockFileSeq().Flush(block_pos_old, fFinalize)) { AbortNode("Flushing block file to disk failed. This is likely the result of an I/O error."); + success = false; } // we do not always flush the undo file, as the chain tip may be lagging behind the incoming blocks, // e.g. during IBD or a sync after a node going offline - if (!fFinalize || finalize_undo) FlushUndoFile(m_last_blockfile, finalize_undo); + if (!fFinalize || finalize_undo) { + if (!FlushUndoFile(blockfile_num, finalize_undo)) { + success = false; + } + } + return success; +} + +BlockfileType BlockManager::BlockfileTypeForHeight(int height) +{ + if (!m_snapshot_height) { + return BlockfileType::NORMAL; + } + return (height >= *m_snapshot_height) ? BlockfileType::ASSUMED : BlockfileType::NORMAL; +} + +bool BlockManager::FlushChainstateBlockFile(int tip_height) +{ + LOCK(cs_LastBlockFile); + auto& cursor = m_blockfile_cursors[BlockfileTypeForHeight(tip_height)]; + if (cursor) { + // The cursor may not exist after a snapshot has been loaded but before any + // blocks have been downloaded. + return FlushBlockFile(cursor->file_num, /*fFinalize=*/false, /*finalize_undo=*/false); + } + return false; } uint64_t BlockManager::CalculateCurrentUsage() @@ -659,8 +747,19 @@ bool BlockManager::FindBlockPos(FlatFilePos& pos, unsigned int nAddSize, unsigne { LOCK(cs_LastBlockFile); - unsigned int nFile = fKnown ? pos.nFile : m_last_blockfile; - if (m_blockfile_info.size() <= nFile) { + const BlockfileType chain_type = BlockfileTypeForHeight(nHeight); + + if (!m_blockfile_cursors[chain_type]) { + // If a snapshot is loaded during runtime, we may not have initialized this cursor yet. + assert(chain_type == BlockfileType::ASSUMED); + const auto new_cursor = BlockfileCursor{this->MaxBlockfileNum() + 1}; + m_blockfile_cursors[chain_type] = new_cursor; + LogPrint(BCLog::BLOCKSTORE, "[%s] initializing blockfile cursor to %s\n", chain_type, new_cursor); + } + const int last_blockfile = m_blockfile_cursors[chain_type]->file_num; + + int nFile = fKnown ? pos.nFile : last_blockfile; + if (static_cast(m_blockfile_info.size()) <= nFile) { m_blockfile_info.resize(nFile + 1); } @@ -677,13 +776,20 @@ bool BlockManager::FindBlockPos(FlatFilePos& pos, unsigned int nAddSize, unsigne } } assert(nAddSize < max_blockfile_size); + while (m_blockfile_info[nFile].nSize + nAddSize >= max_blockfile_size) { // when the undo file is keeping up with the block file, we want to flush it explicitly // when it is lagging behind (more blocks arrive than are being connected), we let the // undo block write case handle it - finalize_undo = (m_blockfile_info[nFile].nHeightLast == m_undo_height_in_last_blockfile); - nFile++; - if (m_blockfile_info.size() <= nFile) { + finalize_undo = (static_cast(m_blockfile_info[nFile].nHeightLast) == + Assert(m_blockfile_cursors[chain_type])->undo_height); + + // Try the next unclaimed blockfile number + nFile = this->MaxBlockfileNum() + 1; + // Set to increment MaxBlockfileNum() for next iteration + m_blockfile_cursors[chain_type] = BlockfileCursor{nFile}; + + if (static_cast(m_blockfile_info.size()) <= nFile) { m_blockfile_info.resize(nFile + 1); } } @@ -691,13 +797,25 @@ bool BlockManager::FindBlockPos(FlatFilePos& pos, unsigned int nAddSize, unsigne pos.nPos = m_blockfile_info[nFile].nSize; } - if ((int)nFile != m_last_blockfile) { + if (nFile != last_blockfile) { if (!fKnown) { - LogPrint(BCLog::BLOCKSTORE, "Leaving block file %i: %s\n", m_last_blockfile, m_blockfile_info[m_last_blockfile].ToString()); + LogPrint(BCLog::BLOCKSTORE, "Leaving block file %i: %s (onto %i) (height %i)\n", + last_blockfile, m_blockfile_info[last_blockfile].ToString(), nFile, nHeight); + } + + // Do not propagate the return code. The flush concerns a previous block + // and undo file that has already been written to. If a flush fails + // here, and we crash, there is no expected additional block data + // inconsistency arising from the flush failure here. However, the undo + // data may be inconsistent after a crash if the flush is called during + // a reindex. A flush error might also leave some of the data files + // untrimmed. + if (!FlushBlockFile(last_blockfile, !fKnown, finalize_undo)) { + LogPrintLevel(BCLog::BLOCKSTORE, BCLog::Level::Warning, "Failed to flush previous block file %05i (finalize=%i, finalize_undo=%i) before opening new block file %05i\n", + last_blockfile, !fKnown, finalize_undo, nFile); } - FlushBlockFile(!fKnown, finalize_undo); - m_last_blockfile = nFile; - m_undo_height_in_last_blockfile = 0; // No undo data yet in the new file, so reset our undo-height tracking. + // No undo data yet in the new file, so reset our undo-height tracking. + m_blockfile_cursors[chain_type] = BlockfileCursor{nFile}; } m_blockfile_info[nFile].AddBlock(nHeight, nTime); @@ -770,6 +888,9 @@ static bool WriteBlockToDisk(const CBlock& block, FlatFilePos& pos, const CMessa bool BlockManager::WriteUndoDataForBlock(const CBlockUndo& blockundo, BlockValidationState& state, CBlockIndex& block) { AssertLockHeld(::cs_main); + const BlockfileType type = BlockfileTypeForHeight(block.nHeight); + auto& cursor = *Assert(WITH_LOCK(cs_LastBlockFile, return m_blockfile_cursors[type])); + // Write undo information to disk if (block.GetUndoPos().IsNull()) { FlatFilePos _pos; @@ -784,10 +905,17 @@ bool BlockManager::WriteUndoDataForBlock(const CBlockUndo& blockundo, BlockValid // in the block file info as below; note that this does not catch the case where the undo writes are keeping up // with the block writes (usually when a synced up node is getting newly mined blocks) -- this case is caught in // the FindBlockPos function - if (_pos.nFile < m_last_blockfile && static_cast(block.nHeight) == m_blockfile_info[_pos.nFile].nHeightLast) { - FlushUndoFile(_pos.nFile, true); - } else if (_pos.nFile == m_last_blockfile && static_cast(block.nHeight) > m_undo_height_in_last_blockfile) { - m_undo_height_in_last_blockfile = block.nHeight; + if (_pos.nFile < cursor.file_num && static_cast(block.nHeight) == m_blockfile_info[_pos.nFile].nHeightLast) { + // Do not propagate the return code, a failed flush here should not + // be an indication for a failed write. If it were propagated here, + // the caller would assume the undo data not to be written, when in + // fact it is. Note though, that a failed flush might leave the data + // file untrimmed. + if (!FlushUndoFile(_pos.nFile, true)) { + LogPrintLevel(BCLog::BLOCKSTORE, BCLog::Level::Warning, "Failed to flush undo file %05i\n", _pos.nFile); + } + } else if (_pos.nFile == cursor.file_num && block.nHeight > cursor.undo_height) { + cursor.undo_height = block.nHeight; } // update nUndoPos in block index block.nUndoPos = _pos.nPos; @@ -955,4 +1083,18 @@ void ThreadImport(ChainstateManager& chainman, std::vector vImportFile } // End scope of CImportingNow chainman.ActiveChainstate().LoadMempool(mempool_path); } + +std::ostream& operator<<(std::ostream& os, const BlockfileType& type) { + switch(type) { + case BlockfileType::NORMAL: os << "normal"; break; + case BlockfileType::ASSUMED: os << "assumed"; break; + default: os.setstate(std::ios_base::failbit); + } + return os; +} + +std::ostream& operator<<(std::ostream& os, const BlockfileCursor& cursor) { + os << strprintf("BlockfileCursor(file_num=%d, undo_height=%d)", cursor.file_num, cursor.undo_height); + return os; +} } // namespace node diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index a7c2fa3987d7..5306e59bbf07 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -75,6 +76,35 @@ struct PruneLockInfo { int height_first{std::numeric_limits::max()}; //! Height of earliest block that should be kept and not pruned }; +enum BlockfileType { + // Values used as array indexes - do not change carelessly. + NORMAL = 0, + ASSUMED = 1, + NUM_TYPES = 2, +}; + +std::ostream& operator<<(std::ostream& os, const BlockfileType& type); + +struct BlockfileCursor { + // The latest blockfile number. + int file_num{0}; + + // Track the height of the highest block in file_num whose undo + // data has been written. Block data is written to block files in download + // order, but is written to undo files in validation order, which is + // usually in order by height. To avoid wasting disk space, undo files will + // be trimmed whenever the corresponding block file is finalized and + // the height of the highest block written to the block file equals the + // height of the highest block written to the undo file. This is a + // heuristic and can sometimes preemptively trim undo files that will write + // more data later, and sometimes fail to trim undo files that can't have + // more data written later. + int undo_height{0}; +}; + +std::ostream& operator<<(std::ostream& os, const BlockfileCursor& cursor); + + /** * Maintains a tree of blocks (stored in `m_block_index`) which is consulted * to determine where the most-work tip is. @@ -92,20 +122,25 @@ class BlockManager const CChainParams& GetParams() const { return m_opts.chainparams; } const Consensus::Params& GetConsensus() const { return m_opts.chainparams.GetConsensus(); } - /** - * Load the blocktree off disk and into memory. Populate certain metadata - * per index entry (nStatus, nChainWork, nTimeMax, etc.) as well as peripheral - * collections like m_dirty_blockindex. - */ - bool LoadBlockIndex() + bool LoadBlockIndex(const std::optional& snapshot_blockhash) EXCLUSIVE_LOCKS_REQUIRED(cs_main); - void FlushBlockFile(bool fFinalize = false, bool finalize_undo = false); - void FlushUndoFile(int block_file, bool finalize = false); - bool FindBlockPos(FlatFilePos& pos, unsigned int nAddSize, unsigned int nHeight, uint64_t nTime, bool fKnown); + + /** Return false if block file or undo file flushing fails. */ + [[nodiscard]] bool FlushBlockFile(int blockfile_num, bool fFinalize, bool finalize_undo); + + /** Return false if undo file flushing fails. */ + [[nodiscard]] bool FlushUndoFile(int block_file, bool finalize = false); + + [[nodiscard]] bool FindBlockPos(FlatFilePos& pos, unsigned int nAddSize, unsigned int nHeight, uint64_t nTime, bool fKnown); + [[nodiscard]] bool FlushChainstateBlockFile(int tip_height); bool FindUndoPos(BlockValidationState& state, int nFile, FlatFilePos& pos, unsigned int nAddSize); /* Calculate the block/rev files to delete based on height specified by user with RPC command pruneblockchain */ - void FindFilesToPruneManual(std::set& setFilesToPrune, int nManualPruneHeight, int chain_tip_height); + void FindFilesToPruneManual( + std::set& setFilesToPrune, + int nManualPruneHeight, + const Chainstate& chain, + ChainstateManager& chainman); /** * Prune block and undo files (blk???.dat and rev???.dat) so that the disk space used is less than a user-defined target. @@ -121,24 +156,39 @@ class BlockManager * A db flag records the fact that at least some block files have been pruned. * * @param[out] setFilesToPrune The set of file indices that can be unlinked will be returned + * @param last_prune The last height we're able to prune, according to the prune locks */ - void FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPruneAfterHeight, int chain_tip_height, int prune_height, bool is_ibd); + void FindFilesToPrune( + std::set& setFilesToPrune, + int last_prune, + const Chainstate& chain, + ChainstateManager& chainman); RecursiveMutex cs_LastBlockFile; std::vector m_blockfile_info; - int m_last_blockfile = 0; - // Track the height of the highest block in m_last_blockfile whose undo - // data has been written. Block data is written to block files in download - // order, but is written to undo files in validation order, which is - // usually in order by height. To avoid wasting disk space, undo files will - // be trimmed whenever the corresponding block file is finalized and - // the height of the highest block written to the block file equals the - // height of the highest block written to the undo file. This is a - // heuristic and can sometimes preemptively trim undo files that will write - // more data later, and sometimes fail to trim undo files that can't have - // more data written later. - unsigned int m_undo_height_in_last_blockfile = 0; + //! Since assumedvalid chainstates may be syncing a range of the chain that is very + //! far away from the normal/background validation process, we should segment blockfiles + //! for assumed chainstates. Otherwise, we might have wildly different height ranges + //! mixed into the same block files, which would impair our ability to prune + //! effectively. + //! + //! This data structure maintains separate blockfile number cursors for each + //! BlockfileType. The ASSUMED state is initialized, when necessary, in FindBlockPos(). + //! + //! The first element is the NORMAL cursor, second is ASSUMED. + std::array, BlockfileType::NUM_TYPES> + m_blockfile_cursors GUARDED_BY(cs_LastBlockFile) = { + BlockfileCursor{}, + std::nullopt, + }; + int MaxBlockfileNum() const EXCLUSIVE_LOCKS_REQUIRED(cs_LastBlockFile) + { + static const BlockfileCursor empty_cursor; + const auto& normal = m_blockfile_cursors[BlockfileType::NORMAL].value_or(empty_cursor); + const auto& assumed = m_blockfile_cursors[BlockfileType::ASSUMED].value_or(empty_cursor); + return std::max(normal.file_num, assumed.file_num); + } /** Global flag to indicate we should check to see if there are * block/undo files that should be deleted. Set on startup @@ -160,6 +210,7 @@ class BlockManager */ std::unordered_map m_prune_locks GUARDED_BY(::cs_main); + BlockfileType BlockfileTypeForHeight(int height); public: using Options = kernel::BlockManagerOpts; @@ -168,6 +219,20 @@ class BlockManager BlockMap m_block_index GUARDED_BY(cs_main); PrevBlockMap m_prev_block_index GUARDED_BY(cs_main); + /** + * The height of the base block of an assumeutxo snapshot, if one is in use. + * + * This controls how blockfiles are segmented by chainstate type to avoid + * comingling different height regions of the chain when an assumedvalid chainstate + * is in use. If heights are drastically different in the same blockfile, pruning + * suffers. + * + * This is set during ActivateSnapshot() or upon LoadBlockIndex() if a snapshot + * had been previously loaded. After the snapshot is validated, this is unset to + * restore normal LoadBlockIndex behavior. + */ + std::optional m_snapshot_height; + std::vector GetAllBlockIndices() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); /** @@ -179,7 +244,8 @@ class BlockManager std::unique_ptr m_block_tree_db GUARDED_BY(::cs_main); bool WriteBlockIndexDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - bool LoadBlockIndexDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + bool LoadBlockIndexDB(const std::optional& snapshot_blockhash) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); /** * Remove any pruned block & undo files that are still on disk. diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 4900dcf785af..3f37bf993f7a 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -1228,9 +1228,9 @@ class NotificationsProxy : public CValidationInterface { m_notifications->transactionRemovedFromMempool(tx, reason); } - void BlockConnected(const std::shared_ptr& block, const CBlockIndex* index) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* index) override { - m_notifications->blockConnected(kernel::MakeBlockInfo(index, block.get())); + m_notifications->blockConnected(role, kernel::MakeBlockInfo(index, block.get())); } void BlockDisconnected(const std::shared_ptr& block, const CBlockIndex* index) override { @@ -1240,7 +1240,10 @@ class NotificationsProxy : public CValidationInterface { m_notifications->updatedBlockTip(); } - void ChainStateFlushed(const CBlockLocator& locator) override { m_notifications->chainStateFlushed(locator); } + void ChainStateFlushed(ChainstateRole role, const CBlockLocator& locator) override + { + m_notifications->chainStateFlushed(role, locator); + } void NotifyChainLock(const CBlockIndex* pindexChainLock, const std::shared_ptr& clsig) override { m_notifications->notifyChainLock(pindexChainLock, clsig); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index bcf89d757bc0..d15391b6e657 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -3170,6 +3171,178 @@ UniValue CreateUTXOSnapshot( return result; } +static RPCHelpMan loadtxoutset() +{ + return RPCHelpMan{ + "loadtxoutset", + "Load the serialized UTXO set from disk.\n" + "Once this snapshot is loaded, its contents will be " + "deserialized into a second chainstate data structure, which is then used to sync to " + "the network's tip under a security model very much like `assumevalid`. " + "Meanwhile, the original chainstate will complete the initial block download process in " + "the background, eventually validating up to the block that the snapshot is based upon.\n\n" + + "The result is a usable bitcoind instance that is current with the network tip in a " + "matter of minutes rather than hours. UTXO snapshot are typically obtained from " + "third-party sources (HTTP, torrent, etc.) which is reasonable since their " + "contents are always checked by hash.\n\n" + + "You can find more information on this process in the `assumeutxo` design " + "document ().", + { + {"path", + RPCArg::Type::STR, + RPCArg::Optional::NO, + "path to the snapshot file. If relative, will be prefixed by datadir."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::NUM, "coins_loaded", "the number of coins loaded from the snapshot"}, + {RPCResult::Type::STR_HEX, "tip_hash", "the hash of the base of the snapshot"}, + {RPCResult::Type::NUM, "base_height", "the height of the base of the snapshot"}, + {RPCResult::Type::STR, "path", "the absolute path that the snapshot was loaded from"}, + } + }, + RPCExamples{ + HelpExampleCli("loadtxoutset", "utxo.dat") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + NodeContext& node = EnsureAnyNodeContext(request.context); + fs::path path{AbsPathForConfigVal(EnsureArgsman(node), fs::u8path(request.params[0].get_str()))}; + + FILE* file{fsbridge::fopen(path, "rb")}; + AutoFile afile{file}; + if (afile.IsNull()) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "Couldn't open file " + path.u8string() + " for reading."); + } + + SnapshotMetadata metadata; + afile >> metadata; + + uint256 base_blockhash = metadata.m_base_blockhash; + int max_secs_to_wait_for_headers = 60 * 10; + CBlockIndex* snapshot_start_block = nullptr; + + LogPrintf("[snapshot] waiting to see blockheader %s in headers chain before snapshot activation\n", + base_blockhash.ToString()); + + ChainstateManager& chainman = *node.chainman; + + while (max_secs_to_wait_for_headers > 0) { + snapshot_start_block = WITH_LOCK(::cs_main, + return chainman.m_blockman.LookupBlockIndex(base_blockhash)); + max_secs_to_wait_for_headers -= 1; + + if (!IsRPCRunning()) { + throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "Shutting down"); + } + + if (!snapshot_start_block) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + } else { + break; + } + } + + if (!snapshot_start_block) { + LogPrintf("[snapshot] timed out waiting for snapshot start blockheader %s\n", + base_blockhash.ToString()); + throw JSONRPCError( + RPC_INTERNAL_ERROR, + "Timed out waiting for base block header to appear in headers chain"); + } + if (!chainman.ActivateSnapshot(afile, metadata, false)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to load UTXO snapshot " + fs::PathToString(path)); + } + CBlockIndex* new_tip{WITH_LOCK(::cs_main, return chainman.ActiveTip())}; + + UniValue result(UniValue::VOBJ); + result.pushKV("coins_loaded", metadata.m_coins_count); + result.pushKV("tip_hash", new_tip->GetBlockHash().ToString()); + result.pushKV("base_height", new_tip->nHeight); + result.pushKV("path", fs::PathToString(path)); + return result; +}, + }; +} + +const std::vector RPCHelpForChainstate{ + {RPCResult::Type::NUM, "blocks", "number of blocks in this chainstate"}, + {RPCResult::Type::STR_HEX, "bestblockhash", "blockhash of the tip"}, + {RPCResult::Type::NUM, "difficulty", "difficulty of the tip"}, + {RPCResult::Type::NUM, "verificationprogress", "progress towards the network tip"}, + {RPCResult::Type::STR_HEX, "snapshot_blockhash", /*optional=*/true, "the base block of the snapshot this chainstate is based on, if any"}, + {RPCResult::Type::NUM, "coins_db_cache_bytes", "size of the coinsdb cache"}, + {RPCResult::Type::NUM, "coins_tip_cache_bytes", "size of the coinstip cache"}, +}; + +static RPCHelpMan getchainstates() +{ +return RPCHelpMan{ + "getchainstates", + "\nReturn information about chainstates.\n", + {}, + RPCResult{ + RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::NUM, "headers", "the number of headers seen so far"}, + {RPCResult::Type::OBJ, "normal", /*optional=*/true, "fully validated chainstate containing blocks this node has validated starting from the genesis block", RPCHelpForChainstate}, + {RPCResult::Type::OBJ, "snapshot", /*optional=*/true, "only present if an assumeutxo snapshot is loaded. Partially validated chainstate containing blocks this node has validated starting from the snapshot. After the snapshot is validated (when the 'normal' chainstate advances far enough to validate it), this chainstate will replace and become the 'normal' chainstate.", RPCHelpForChainstate}, + } + }, + RPCExamples{ + HelpExampleCli("getchainstates", "") + + HelpExampleRpc("getchainstates", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + LOCK(cs_main); + UniValue obj(UniValue::VOBJ); + + NodeContext& node = EnsureAnyNodeContext(request.context); + ChainstateManager& chainman = *node.chainman; + + auto make_chain_data = [&](const Chainstate& cs) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + AssertLockHeld(::cs_main); + UniValue data(UniValue::VOBJ); + if (!cs.m_chain.Tip()) { + return data; + } + const CChain& chain = cs.m_chain; + const CBlockIndex* tip = chain.Tip(); + + data.pushKV("blocks", (int)chain.Height()); + data.pushKV("bestblockhash", tip->GetBlockHash().GetHex()); + data.pushKV("difficulty", (double)GetDifficulty(tip)); + data.pushKV("verificationprogress", GuessVerificationProgress(Params().TxData(), tip)); + data.pushKV("coins_db_cache_bytes", cs.m_coinsdb_cache_size_bytes); + data.pushKV("coins_tip_cache_bytes", cs.m_coinstip_cache_size_bytes); + if (cs.m_from_snapshot_blockhash) { + data.pushKV("snapshot_blockhash", cs.m_from_snapshot_blockhash->ToString()); + } + return data; + }; + + if (chainman.GetAll().size() > 1) { + for (Chainstate* chainstate : chainman.GetAll()) { + obj.pushKV( + chainstate->m_from_snapshot_blockhash ? "snapshot" : "normal", + make_chain_data(*chainstate)); + } + } else { + obj.pushKV("normal", make_chain_data(chainman.ActiveChainstate())); + } + obj.pushKV("headers", chainman.m_best_header ? chainman.m_best_header->nHeight : -1); + + return obj; +} + }; +} + + void RegisterBlockchainRPCCommands(CRPCTable& t) { static const CRPCCommand commands[]{ @@ -3198,13 +3371,15 @@ void RegisterBlockchainRPCCommands(CRPCTable& t) {"blockchain", &scantxoutset}, {"blockchain", &scanblocks}, {"blockchain", &getblockfilter}, + {"blockchain", &dumptxoutset}, + {"blockchain", &loadtxoutset}, + {"blockchain", &getchainstates}, {"hidden", &invalidateblock}, {"hidden", &reconsiderblock}, {"hidden", &waitfornewblock}, {"hidden", &waitforblock}, {"hidden", &waitforblockheight}, {"hidden", &syncwithvalidationinterfacequeue}, - {"hidden", &dumptxoutset}, }; for (const auto& c : commands) { t.appendCommand(c.name, &c); diff --git a/src/test/coinstatsindex_tests.cpp b/src/test/coinstatsindex_tests.cpp index de9a01bb1a91..5a5c088d34f5 100644 --- a/src/test/coinstatsindex_tests.cpp +++ b/src/test/coinstatsindex_tests.cpp @@ -104,7 +104,7 @@ BOOST_FIXTURE_TEST_CASE(coinstatsindex_unclean_shutdown, TestChain100Setup) // Send block connected notification, then stop the index without // sending a chainstate flushed notification. Prior to #24138, this // would cause the index to be corrupted and fail to reload. - ValidationInterfaceTest::BlockConnected(index, new_block, new_block_index); + ValidationInterfaceTest::BlockConnected(ChainstateRole::NORMAL, index, new_block, new_block_index); index.Stop(); } diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index 961efef4d796..72a2e230cb77 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -78,6 +78,7 @@ const std::vector RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{ "generatetodescriptor", // avoid prohibitively slow execution (when `nblocks` is large) "gettxoutproof", // avoid prohibitively slow execution "importwallet", // avoid reading from disk + "loadtxoutset", // avoid reading from disk "loadwallet", // avoid reading from disk "savemempool", // disabled as a precautionary measure: may take a file path argument in the future "setban", // avoid DNS lookups @@ -118,6 +119,7 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "getblockstats", "getblocktemplate", "getchaintips", + "getchainstates", "getchaintxstats", "getconnectioncount", "getdeploymentinfo", diff --git a/src/test/util/chainstate.h b/src/test/util/chainstate.h index 6b589b597243..55d12f9ed007 100644 --- a/src/test/util/chainstate.h +++ b/src/test/util/chainstate.h @@ -110,7 +110,23 @@ CreateAndActivateUTXOSnapshot( 0 == WITH_LOCK(node.chainman->GetMutex(), return node.chainman->ActiveHeight())); } - return node.chainman->ActivateSnapshot(auto_infile, metadata, in_memory_chainstate); + auto& new_active = node.chainman->ActiveChainstate(); + auto* tip = new_active.m_chain.Tip(); + + // Disconnect a block so that the snapshot chainstate will be ahead, otherwise + // it will refuse to activate. + // + // TODO this is a unittest-specific hack, and we should probably rethink how to + // better generate/activate snapshots in unittests. + if (tip->pprev) { + new_active.m_chain.SetTip(*(tip->pprev)); + } + + bool res = node.chainman->ActivateSnapshot(auto_infile, metadata, in_memory_chainstate); + + // Restore the old tip. + new_active.m_chain.SetTip(*tip); + return res; } diff --git a/src/test/util/validation.cpp b/src/test/util/validation.cpp index 49535855f988..fa281f04bd67 100644 --- a/src/test/util/validation.cpp +++ b/src/test/util/validation.cpp @@ -22,7 +22,11 @@ void TestChainState::JumpOutOfIbd() Assert(!IsInitialBlockDownload()); } -void ValidationInterfaceTest::BlockConnected(CValidationInterface& obj, const std::shared_ptr& block, const CBlockIndex* pindex) +void ValidationInterfaceTest::BlockConnected( + ChainstateRole role, + CValidationInterface& obj, + const std::shared_ptr& block, + const CBlockIndex* pindex) { - obj.BlockConnected(block, pindex); + obj.BlockConnected(role, block, pindex); } diff --git a/src/test/util/validation.h b/src/test/util/validation.h index cbe7745b81e4..87a5b72fea7b 100644 --- a/src/test/util/validation.h +++ b/src/test/util/validation.h @@ -19,7 +19,11 @@ struct TestChainState : public Chainstate { class ValidationInterfaceTest { public: - static void BlockConnected(CValidationInterface& obj, const std::shared_ptr& block, const CBlockIndex* pindex); + static void BlockConnected( + ChainstateRole role, + CValidationInterface& obj, + const std::shared_ptr& block, + const CBlockIndex* pindex); }; #endif // BITCOIN_TEST_UTIL_VALIDATION_H diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp index 555c87c09b96..74bc0faf34aa 100644 --- a/src/test/validation_block_tests.cpp +++ b/src/test/validation_block_tests.cpp @@ -45,7 +45,7 @@ struct TestSubscriber final : public CValidationInterface { BOOST_CHECK_EQUAL(m_expected_tip, pindexNew->GetBlockHash()); } - void BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) override { BOOST_CHECK_EQUAL(m_expected_tip, block->hashPrevBlock); BOOST_CHECK_EQUAL(m_expected_tip, pindex->pprev->GetBlockHash()); diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 5b466a722f5f..16d6643405c2 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -51,7 +51,7 @@ void SeedSnapshotMarker(CEvoDB& evodb, const uint256& hash) } // namespace -BOOST_FIXTURE_TEST_SUITE(validation_chainstatemanager_tests, ChainTestingSetup) +BOOST_FIXTURE_TEST_SUITE(validation_chainstatemanager_tests, TestingSetup) static void DashChainstateSetup(ChainstateManager& chainman, node::NodeContext& node, @@ -78,10 +78,9 @@ static void DashChainstateSetupClose(node::NodeContext& node) //! Basic tests for ChainstateManager. //! //! First create a legacy (IBD) chainstate, then create a snapshot chainstate. -BOOST_AUTO_TEST_CASE(chainstatemanager) +BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup) { ChainstateManager& manager = *m_node.chainman; - CTxMemPool& mempool = *m_node.mempool; CEvoDB& evodb = *m_node.evodb; std::vector chainstates; @@ -89,17 +88,8 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) // Create a legacy (IBD) chainstate. // - Chainstate& c1 = WITH_LOCK(::cs_main, return manager.InitializeChainstate(&mempool, evodb, m_node.chain_helper)); + Chainstate& c1 = manager.ActiveChainstate(); chainstates.push_back(&c1); - c1.InitCoinsDB( - /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); - WITH_LOCK(::cs_main, c1.InitCoinsCache(1 << 23)); - - DashChainstateSetup(manager, m_node, /*llmq_dbs_in_memory=*/true, /*llmq_dbs_wipe=*/false); - - BOOST_REQUIRE(c1.LoadGenesisBlock()); - BlockValidationState val_state; - BOOST_CHECK(c1.ActivateBestChain(val_state, nullptr)); BOOST_CHECK(!manager.IsSnapshotActive()); BOOST_CHECK(!manager.IsSnapshotActiveAndUnvalidated()); @@ -111,8 +101,9 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) auto& active_chain = WITH_LOCK(manager.GetMutex(), return manager.ActiveChain()); BOOST_CHECK_EQUAL(&active_chain, &c1.m_chain); - BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 0); - + // Get to a valid assumeutxo tip (per chainparams); + mineBlocks(10); + BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 110); auto active_tip = WITH_LOCK(manager.GetMutex(), return manager.ActiveTip()); auto exp_tip = c1.m_chain.Tip(); BOOST_CHECK_EQUAL(active_tip, exp_tip); @@ -125,10 +116,7 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) // const uint256 snapshot_blockhash = active_tip->GetBlockHash(); SeedSnapshotMarker(evodb, snapshot_blockhash); - Chainstate* c2_ptr = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot( - &mempool, snapshot_blockhash)); - BOOST_REQUIRE(c2_ptr); - Chainstate& c2 = *c2_ptr; + Chainstate& c2 = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot(snapshot_blockhash)); chainstates.push_back(&c2); // Only the active chainstate keeps the mempool. @@ -138,14 +126,19 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) DashChainstateSetup(manager, m_node, /*llmq_dbs_in_memory=*/true, /*llmq_dbs_wipe=*/false); BOOST_CHECK_EQUAL(manager.SnapshotBlockhash().value(), snapshot_blockhash); - c2.InitCoinsDB( /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); - WITH_LOCK(::cs_main, c2.InitCoinsCache(1 << 23)); - c2.m_chain.SetTip(*active_tip); + { + LOCK(::cs_main); + c2.InitCoinsCache(1 << 23); + c2.CoinsTip().SetBestBlock(active_tip->GetBlockHash()); + c2.setBlockIndexCandidates.insert(manager.m_blockman.LookupBlockIndex(active_tip->GetBlockHash())); + c2.LoadChainTip(); + } BlockValidationState _; BOOST_CHECK(c2.ActivateBestChain(_, nullptr)); + BOOST_CHECK_EQUAL(manager.SnapshotBlockhash().value(), snapshot_blockhash); BOOST_CHECK(manager.IsSnapshotActive()); BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.IsSnapshotValidated())); BOOST_CHECK(manager.IsSnapshotActiveAndUnvalidated()); @@ -158,13 +151,15 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) auto& active_chain2 = WITH_LOCK(manager.GetMutex(), return manager.ActiveChain()); BOOST_CHECK_EQUAL(&active_chain2, &c2.m_chain); - BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 0); + BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 110); + mineBlocks(1); + BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 111); + BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return c1.m_chain.Height()), 110); auto active_tip2 = WITH_LOCK(manager.GetMutex(), return manager.ActiveTip()); - auto exp_tip2 = c2.m_chain.Tip(); - BOOST_CHECK_EQUAL(active_tip2, exp_tip2); - - BOOST_CHECK_EQUAL(exp_tip, exp_tip2); + BOOST_CHECK_EQUAL(active_tip, active_tip2->pprev); + BOOST_CHECK_EQUAL(active_tip, c1.m_chain.Tip()); + BOOST_CHECK_EQUAL(active_tip2, c2.m_chain.Tip()); // Let scheduler events finish running to avoid accessing memory that is going to be unloaded SyncWithValidationInterfaceQueue(); @@ -229,7 +224,6 @@ BOOST_FIXTURE_TEST_CASE(snapshot_prune_lock_release_survives_disconnect, TestCha BOOST_FIXTURE_TEST_CASE(chainstatemanager_rebalance_caches, TestChain100Setup) { ChainstateManager& manager = *m_node.chainman; - CTxMemPool& mempool = *m_node.mempool; CEvoDB& evodb = *m_node.evodb; size_t max_cache = 10000; manager.m_total_coinsdb_cache = max_cache; @@ -241,9 +235,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_rebalance_caches, TestChain100Setup) // Chainstate& c1 = manager.ActiveChainstate(); chainstates.push_back(&c1); - c1.InitCoinsDB( - /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); - { LOCK(::cs_main); c1.InitCoinsCache(1 << 23); @@ -257,9 +248,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_rebalance_caches, TestChain100Setup) // CBlockIndex* snapshot_base{WITH_LOCK(manager.GetMutex(), return manager.ActiveChain()[manager.ActiveChain().Height() / 2])}; SeedSnapshotMarker(evodb, snapshot_base->GetBlockHash()); - Chainstate* c2_ptr = WITH_LOCK(cs_main, return manager.ActivateExistingSnapshot(&mempool, snapshot_base->GetBlockHash())); - BOOST_REQUIRE(c2_ptr); - Chainstate& c2 = *c2_ptr; + Chainstate& c2 = WITH_LOCK(cs_main, return manager.ActivateExistingSnapshot(*snapshot_base->phashBlock)); chainstates.push_back(&c2); c2.InitCoinsDB( /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); @@ -411,10 +400,10 @@ struct SnapshotTestSetup : TestChain100Setup { BOOST_CHECK(!chainman.ActiveChain().Genesis()->IsAssumedValid()); } - const AssumeutxoData& au_data = *ExpectedAssumeutxo(snapshot_height, ::Params()); + const auto& au_data = ::Params().AssumeutxoForHeight(snapshot_height); const CBlockIndex* tip = WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip()); - BOOST_CHECK_EQUAL(tip->nChainTx, au_data.nChainTx); + BOOST_CHECK_EQUAL(tip->nChainTx, au_data->nChainTx); // Make some assertions about the both chainstates. These checks ensure the // legacy chainstate hasn't changed and that the newly created chainstate @@ -589,18 +578,24 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_reconsider_block_candidates, SnapshotT BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) { ChainstateManager& chainman = *Assert(m_node.chainman); - CTxMemPool& mempool = *m_node.mempool; Chainstate& cs1 = chainman.ActiveChainstate(); int num_indexes{0}; int num_assumed_valid{0}; + // Blocks in range [assumed_valid_start_idx, last_assumed_valid_idx) will be + // marked as assumed-valid and not having data. const int expected_assumed_valid{20}; - const int last_assumed_valid_idx{40}; + const int last_assumed_valid_idx{111}; const int assumed_valid_start_idx = last_assumed_valid_idx - expected_assumed_valid; + // Mine to height 120, past the hardcoded regtest assumeutxo snapshot at + // height 110 + mineBlocks(20); + CBlockIndex* validated_tip{nullptr}; CBlockIndex* assumed_base{nullptr}; CBlockIndex* assumed_tip{WITH_LOCK(chainman.GetMutex(), return chainman.ActiveChain().Tip())}; + BOOST_CHECK_EQUAL(assumed_tip->nHeight, 120); auto reload_all_block_indexes = [&]() { // For completeness, we also reset the block sequence counters to @@ -626,7 +621,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) LOCK(::cs_main); auto index = cs1.m_chain[i]; - // Blocks with heights in range [20, 40) are marked ASSUMED_VALID + // Blocks with heights in range [91, 110] are marked ASSUMED_VALID if (i < last_assumed_valid_idx && i >= assumed_valid_start_idx) { index->nStatus = BlockStatus::BLOCK_VALID_TREE | BlockStatus::BLOCK_ASSUMED_VALID; } @@ -652,10 +647,9 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) const uint256 snapshot_blockhash = assumed_base->GetBlockHash(); SeedSnapshotMarker(*m_node.evodb, snapshot_blockhash); - Chainstate* cs2_ptr = WITH_LOCK(::cs_main, - return chainman.ActivateExistingSnapshot(&mempool, snapshot_blockhash)); - BOOST_REQUIRE(cs2_ptr); - Chainstate& cs2 = *cs2_ptr; + // Note: cs2's tip is not set when ActivateExistingSnapshot is called. + Chainstate& cs2 = WITH_LOCK(::cs_main, + return chainman.ActivateExistingSnapshot(*assumed_base->phashBlock)); // Note: cs2's tip is not set when ActivateExistingSnapshot is called. // Set tip of the fully validated chain to be the validated tip @@ -664,10 +658,36 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) // Set tip of the assume-valid-based chain to the assume-valid block cs2.m_chain.SetTip(*assumed_base); + // Sanity check test variables. + BOOST_CHECK_EQUAL(num_indexes, 121); // 121 total blocks, including genesis + BOOST_CHECK_EQUAL(assumed_tip->nHeight, 120); // original chain has height 120 + BOOST_CHECK_EQUAL(validated_tip->nHeight, 90); // current cs1 chain has height 90 + BOOST_CHECK_EQUAL(assumed_base->nHeight, 110); // current cs2 chain has height 110 + + // Regenerate cs1.setBlockIndexCandidates and cs2.setBlockIndexCandidate and + // check contents below. reload_all_block_indexes(); - // The fully validated chain should have the current validated tip - // and the assumed valid base as candidates. + // The fully validated chain should only have the current validated tip and + // the assumed valid base as candidates, blocks 90 and 110. Specifically: + // + // - It does not have blocks 0-89 because they contain less work than the + // chain tip. + // + // - It has block 90 because it has data and equal work to the chain tip, + // (since it is the chain tip). + // + // - It does not have blocks 91-109 because they do not contain data. + // + // - It has block 110 even though it does not have data, because + // LoadBlockIndex has a special case to always add the snapshot block as a + // candidate. The special case is only actually intended to apply to the + // snapshot chainstate cs2, not the background chainstate cs1, but it is + // written broadly and applies to both. + // + // - It does not have any blocks after height 110 because cs1 is a background + // chainstate, and only blocks where are ancestors of the snapshot block + // are added as candidates for the background chainstate. BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.size(), 2); BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.count(validated_tip), 1); BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.count(assumed_base), 1); @@ -675,8 +695,25 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) // The assumed-valid tolerant chain has the assumed valid base as a // candidate, but otherwise has none of the assumed-valid (which do not // HAVE_DATA) blocks as candidates. + // + // Specifically: + // - All blocks below height 110 are not candidates, because cs2 chain tip + // has height 110 and they have less work than it does. + // + // - Block 110 is a candidate even though it does not have data, because it + // is the snapshot block, which is assumed valid. + // + // - Blocks 111-120 are added because they have data. + + // Check that block 90 is absent BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(validated_tip), 0); + // Check that block 109 is absent + BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_base->pprev), 0); + // Check that block 110 is present + BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_base), 1); + // Check that block 120 is present BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_tip), 1); + // Check that 11 blocks total are present. BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.size(), num_indexes - last_assumed_valid_idx + 1); } diff --git a/src/test/validation_tests.cpp b/src/test/validation_tests.cpp index 60263280834a..0784ecc5e99f 100644 --- a/src/test/validation_tests.cpp +++ b/src/test/validation_tests.cpp @@ -27,15 +27,15 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo) std::vector bad_heights{0, 100, 111, 115, 209, 211}; for (auto empty : bad_heights) { - const auto out = ExpectedAssumeutxo(empty, *params); + const auto out = params->AssumeutxoForHeight(empty); BOOST_CHECK(!out); } - const auto out110 = *ExpectedAssumeutxo(110, *params); + const auto out110 = *params->AssumeutxoForHeight(110); BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b"); BOOST_CHECK_EQUAL(out110.nChainTx, 110U); - const auto out210 = *ExpectedAssumeutxo(200, *params); + const auto out210 = *params->AssumeutxoForHeight(200); BOOST_CHECK_EQUAL(out210.hash_serialized.ToString(), "8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3"); BOOST_CHECK_EQUAL(out210.nChainTx, 200U); } diff --git a/src/test/validationinterface_tests.cpp b/src/test/validationinterface_tests.cpp index 11f8c5a53ee9..eb97041d1362 100644 --- a/src/test/validationinterface_tests.cpp +++ b/src/test/validationinterface_tests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include BOOST_FIXTURE_TEST_SUITE(validationinterface_tests, ChainTestingSetup) diff --git a/src/util/vector.h b/src/util/vector.h index bc62b64d280e..4284bed4bd50 100644 --- a/src/util/vector.h +++ b/src/util/vector.h @@ -5,7 +5,9 @@ #ifndef BITCOIN_UTIL_VECTOR_H #define BITCOIN_UTIL_VECTOR_H +#include #include +#include #include #include #include @@ -67,4 +69,15 @@ inline void ClearShrink(V& v) noexcept V{}.swap(v); } +template +inline std::optional FindFirst(const std::vector& vec, const L fnc) +{ + for (const auto& el : vec) { + if (fnc(el)) { + return el; + } + } + return std::nullopt; +} + #endif // BITCOIN_UTIL_VECTOR_H diff --git a/src/validation.cpp b/src/validation.cpp index 907e33f25f9e..b885e65d99af 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -6,6 +6,7 @@ #include +#include #include #include @@ -73,6 +74,8 @@ #include #include #include +#include +#include using kernel::CCoinsStats; using kernel::CoinStatsHashType; @@ -2666,11 +2669,14 @@ bool Chainstate::FlushStateToDisk( if (nManualPruneHeight > 0) { LOG_TIME_MILLIS_WITH_CATEGORY("find files to prune (manual)", BCLog::BENCHMARK); - m_blockman.FindFilesToPruneManual(setFilesToPrune, std::min(last_prune, nManualPruneHeight), m_chain.Height()); + m_blockman.FindFilesToPruneManual( + setFilesToPrune, + std::min(last_prune, nManualPruneHeight), + *this, m_chainman); } else { LOG_TIME_MILLIS_WITH_CATEGORY("find files to prune", BCLog::BENCHMARK); - m_blockman.FindFilesToPrune(setFilesToPrune, m_params.PruneAfterHeight(), m_chain.Height(), last_prune, IsInitialBlockDownload()); + m_blockman.FindFilesToPrune(setFilesToPrune, last_prune, *this, m_chainman); m_blockman.m_check_for_pruning = false; } if (!setFilesToPrune.empty()) { @@ -2712,7 +2718,11 @@ bool Chainstate::FlushStateToDisk( LOG_TIME_MILLIS_WITH_CATEGORY("write block and undo data to disk", BCLog::BENCHMARK); // First make sure all block and undo data is flushed to disk. - m_blockman.FlushBlockFile(); + // TODO: Handle return error, or add detailed comment why it is + // safe to not return an error upon failure. + if (!m_blockman.FlushChainstateBlockFile(m_chain.Height())) { + LogPrintLevel(BCLog::VALIDATION, BCLog::Level::Warning, "%s: Failed to flush block file.\n", __func__); + } } // Then update all block file information (which may refer to block and undo files). @@ -2765,10 +2775,9 @@ bool Chainstate::FlushStateToDisk( (bool)fFlushForPrune); } } - if (full_flush_completed && this == &m_chainman.ActiveChainstate()) { + if (full_flush_completed) { // Update best block in wallet (so we can detect restored wallets). - // TODO(assumeutxo): upstream tags this notification with ChainstateRole instead of suppressing; adopt when backporting index/wallet assumeutxo support. - GetMainSignals().ChainStateFlushed(m_chain.GetLocator()); + GetMainSignals().ChainStateFlushed(this->GetRole(), m_chain.GetLocator()); } } catch (const std::runtime_error& e) { return AbortNode(state, std::string("System error while flushing: ") + e.what()); @@ -3389,6 +3398,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< CBlockIndex *pindexMostWork = nullptr; CBlockIndex *pindexNewTip = nullptr; int nStopAtHeight = gArgs.GetIntArg("-stopatheight", DEFAULT_STOPATHEIGHT); + bool exited_ibd{false}; do { // Block until the validation queue drains. This should largely // never happen in normal operation, however may happen during @@ -3402,6 +3412,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< LOCK(cs_main); // Lock transaction pool for at least as long as it takes for connectTrace to be consumed LOCK(MempoolMutex()); + const bool was_in_ibd = m_chainman.IsInitialBlockDownload(); CBlockIndex* starting_tip = m_chain.Tip(); bool blocks_connected = false; do { @@ -3432,11 +3443,9 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< } pindexNewTip = m_chain.Tip(); - if (this == &m_chainman.ActiveChainstate()) { - for (const PerBlockConnectTrace& trace : connectTrace.GetBlocksConnected()) { - assert(trace.pblock && trace.pindex); - GetMainSignals().BlockConnected(trace.pblock, trace.pindex); - } + for (const PerBlockConnectTrace& trace : connectTrace.GetBlocksConnected()) { + assert(trace.pblock && trace.pindex); + GetMainSignals().BlockConnected(this->GetRole(), trace.pblock, trace.pindex); } // This will have been toggled in @@ -3451,25 +3460,46 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< if (!blocks_connected) return true; const CBlockIndex* pindexFork = m_chain.FindFork(starting_tip); - bool fInitialDownload = IsInitialBlockDownload(); + bool still_in_ibd = m_chainman.IsInitialBlockDownload(); + + if (was_in_ibd && !still_in_ibd) { + // Active chainstate has exited IBD. + exited_ibd = true; + } // Notify external listeners about the new tip. // Enqueue while holding cs_main to ensure that UpdatedBlockTip is called in the order in which blocks are connected if (this == &m_chainman.ActiveChainstate() && pindexFork != pindexNewTip) { // Notify ValidationInterface subscribers - GetMainSignals().SynchronousUpdatedBlockTip(pindexNewTip, pindexFork, fInitialDownload); - GetMainSignals().UpdatedBlockTip(pindexNewTip, pindexFork, fInitialDownload); + GetMainSignals().SynchronousUpdatedBlockTip(pindexNewTip, pindexFork, still_in_ibd); + GetMainSignals().UpdatedBlockTip(pindexNewTip, pindexFork, still_in_ibd); // Always notify the UI if a new block tip was connected - uiInterface.NotifyBlockTip(GetSynchronizationState(fInitialDownload), pindexNewTip); + uiInterface.NotifyBlockTip(GetSynchronizationState(still_in_ibd), pindexNewTip); } } // When we reach this point, we switched to a new tip (stored in pindexNewTip). if (nStopAtHeight && pindexNewTip && pindexNewTip->nHeight >= nStopAtHeight) StartShutdown(); + if (exited_ibd) { + // If a background chainstate is in use, we may need to rebalance our + // allocation of caches once a chainstate exits initial block download. + LOCK(::cs_main); + m_chainman.MaybeRebalanceCaches(); + } if (WITH_LOCK(::cs_main, return m_disabled)) { // Background chainstate has reached the snapshot base block, so exit. + + // Restart indexes to resume indexing for all blocks unique to the snapshot + // chain. This resumes indexing "in order" from where the indexing on the + // background validation chain left off. + // + // This cannot be done while holding cs_main (within + // MaybeCompleteSnapshotValidation) or a cs_main deadlock will occur. + if (m_chainman.restart_indexes) { + m_chainman.restart_indexes(); + } break; } @@ -3880,7 +3910,8 @@ void Chainstate::TryAddBlockIndexCandidate(CBlockIndex* pindex) if (pindex->nStatus & BLOCK_CONFLICT_CHAINLOCK) { return; } - // The block only is a candidate for the most-work-chain if it has more work than our current tip. + // The block only is a candidate for the most-work-chain if it has the same + // or more work than our current tip. if (m_chain.Tip() != nullptr && setBlockIndexCandidates.value_comp()(pindex, m_chain.Tip())) { return; } @@ -4522,6 +4553,12 @@ bool ChainstateManager::ProcessNewBlock(const std::shared_ptr& blo return error("%s: ActivateBestChain failed: %s", __func__, state.ToString()); LogPrintf("%s : ACCEPTED\n", __func__); + Chainstate* bg_chain{WITH_LOCK(cs_main, return BackgroundSyncInProgress() ? m_ibd_chainstate.get() : nullptr)}; + BlockValidationState bg_state; + if (bg_chain && !bg_chain->ActivateBestChain(bg_state, block)) { + return error("%s: [background] ActivateBestChain failed (%s)", __func__, bg_state.ToString()); + } + return true; } @@ -4903,7 +4940,7 @@ bool ChainstateManager::LoadBlockIndex() // Load block index from databases bool needs_init = fReindex; if (!fReindex) { - bool ret{m_blockman.LoadBlockIndexDB()}; + bool ret{m_blockman.LoadBlockIndexDB(SnapshotBlockhash())}; if (!ret) return false; m_blockman.ScanAndUnlinkAlreadyPrunedFiles(); @@ -5231,6 +5268,10 @@ void ChainstateManager::CheckBlockIndex() CBlockIndex* pindexFirstAssumeValid = nullptr; // Oldest ancestor of pindex which has BLOCK_ASSUMED_VALID while (pindex != nullptr) { nNodes++; + if (pindex->pprev && pindex->nTx > 0) { + // nChainTx should increase monotonically + assert(pindex->pprev->nChainTx <= pindex->nChainTx); + } if (pindexFirstAssumeValid == nullptr && pindex->nStatus & BLOCK_ASSUMED_VALID) pindexFirstAssumeValid = pindex; if (pindexFirstInvalid == nullptr && pindex->nStatus & BLOCK_FAILED_VALID) pindexFirstInvalid = pindex; if (pindexFirstConflicing == nullptr && pindex->nStatus & BLOCK_CONFLICT_CHAINLOCK) pindexFirstConflicing = pindex; @@ -5538,19 +5579,7 @@ Chainstate& ChainstateManager::InitializeChainstate(CTxMemPool* mempool, return *m_active_chainstate; } -const AssumeutxoData* ExpectedAssumeutxo( - const int height, const CChainParams& chainparams) -{ - const MapAssumeutxo& valid_assumeutxos_map = chainparams.Assumeutxo(); - const auto assumeutxo_found = valid_assumeutxos_map.find(height); - - if (assumeutxo_found != valid_assumeutxos_map.end()) { - return &assumeutxo_found->second; - } - return nullptr; -} - -static bool DeleteCoinsDBFromDisk(const fs::path db_path, bool is_snapshot) +[[nodiscard]] static bool DeleteCoinsDBFromDisk(const fs::path db_path, bool is_snapshot) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); @@ -5615,6 +5644,14 @@ bool ChainstateManager::ActivateSnapshot( return false; } + { + LOCK(::cs_main); + if (Assert(m_active_chainstate->GetMempool())->size() > 0) { + LogPrintf("[snapshot] can't activate a snapshot when mempool not empty\n"); + return false; + } + } + int64_t current_coinsdb_cache_size{0}; int64_t current_coinstip_cache_size{0}; @@ -5664,19 +5701,8 @@ bool ChainstateManager::ActivateSnapshot( static_cast(current_coinstip_cache_size * SNAPSHOT_CACHE_PERC)); } - bool snapshot_ok = this->PopulateAndValidateSnapshot( - *snapshot_chainstate, coins_file, metadata); - - // If not in-memory, persist the base blockhash for use during subsequent - // initialization. - if (!in_memory) { - LOCK(::cs_main); - if (!node::WriteSnapshotBaseBlockhash(*snapshot_chainstate)) { - snapshot_ok = false; - } - } - if (!snapshot_ok) { - LOCK(::cs_main); + auto cleanup_bad_snapshot = [&](const char* reason) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + LogPrintf("[snapshot] activation failed - %s\n", reason); this->ReleaseSnapshotPruneLock(); this->MaybeRebalanceCaches(); @@ -5709,35 +5735,49 @@ bool ChainstateManager::ActivateSnapshot( } } return false; - } + }; - { + if (!this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata)) { LOCK(::cs_main); - assert(!m_snapshot_chainstate); - m_snapshot_chainstate.swap(snapshot_chainstate); - const bool chaintip_loaded = m_snapshot_chainstate->LoadChainTip(); - assert(chaintip_loaded); + return cleanup_bad_snapshot("population failed"); + } - m_active_chainstate = m_snapshot_chainstate.get(); - m_snapshot_chainstate->m_evoDb.SetDefaultIdentity(EvoDbIdentity::SNAPSHOT); + LOCK(::cs_main); // cs_main required for rest of snapshot activation. - // Move the mempool to the snapshot chainstate: only the active - // chainstate keeps one, so background block connects cannot touch - // mempool state built on the snapshot tip. The mempool is empty at - // this point because snapshot activation happens during IBD. - Assume(!m_snapshot_chainstate->m_mempool); - if (m_ibd_chainstate->m_mempool) { - Assume(m_ibd_chainstate->m_mempool->size() == 0); - m_snapshot_chainstate->m_mempool = m_ibd_chainstate->m_mempool; - m_ibd_chainstate->m_mempool = nullptr; + // Do a final check to ensure that the snapshot chainstate is actually a more + // work chain than the active chainstate; a user could have loaded a snapshot + // very late in the IBD process, and we wouldn't want to load a useless chainstate. + if (!CBlockIndexWorkComparator()(ActiveTip(), snapshot_chainstate->m_chain.Tip())) { + return cleanup_bad_snapshot("work does not exceed active chainstate"); + } + // If not in-memory, persist the base blockhash for use during subsequent + // initialization. + if (!in_memory) { + if (!node::WriteSnapshotBaseBlockhash(*snapshot_chainstate)) { + return cleanup_bad_snapshot("could not write base blockhash"); } + } + + assert(!m_snapshot_chainstate); + m_snapshot_chainstate.swap(snapshot_chainstate); + const bool chaintip_loaded = m_snapshot_chainstate->LoadChainTip(); + assert(chaintip_loaded); + + // Transfer possession of the mempool to the snapshot chainstate. + // Mempool is empty at this point because we're still in IBD. + Assert(m_active_chainstate->m_mempool->size() == 0); + Assert(!m_snapshot_chainstate->m_mempool); + m_snapshot_chainstate->m_mempool = m_active_chainstate->m_mempool; + m_active_chainstate->m_mempool = nullptr; + m_active_chainstate = m_snapshot_chainstate.get(); + m_snapshot_chainstate->m_evoDb.SetDefaultIdentity(EvoDbIdentity::SNAPSHOT); + m_blockman.m_snapshot_height = this->GetSnapshotBaseHeight(); - LogPrintf("[snapshot] successfully activated snapshot %s\n", base_blockhash.ToString()); - LogPrintf("[snapshot] (%.2f MB)\n", - m_snapshot_chainstate->CoinsTip().DynamicMemoryUsage() / (1000 * 1000)); + LogPrintf("[snapshot] successfully activated snapshot %s\n", base_blockhash.ToString()); + LogPrintf("[snapshot] (%.2f MB)\n", + m_snapshot_chainstate->CoinsTip().DynamicMemoryUsage() / (1000 * 1000)); - this->MaybeRebalanceCaches(); - } + this->MaybeRebalanceCaches(); return true; } @@ -5779,7 +5819,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot( CBlockIndex* snapshot_start_block = WITH_LOCK(::cs_main, return m_blockman.LookupBlockIndex(base_blockhash)); if (!snapshot_start_block) { - // Needed for ComputeUTXOStats and ExpectedAssumeutxo to determine the + // Needed for ComputeUTXOStats and AssumeutxoForHeight to determine the // height and to avoid a crash when base_blockhash.IsNull() LogPrintf("[snapshot] Did not find snapshot start blockheader %s\n", base_blockhash.ToString()); @@ -5792,7 +5832,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot( "assumeutxo", {.height_first = snapshot_start_block->nHeight})); int base_height = snapshot_start_block->nHeight; - auto maybe_au_data = ExpectedAssumeutxo(base_height, GetParams()); + auto maybe_au_data = GetParams().AssumeutxoForHeight(base_height); if (!maybe_au_data) { LogPrintf("[snapshot] assumeutxo height in snapshot metadata not recognized " /* Continued */ @@ -5802,6 +5842,14 @@ bool ChainstateManager::PopulateAndValidateSnapshot( const AssumeutxoData& au_data = *maybe_au_data; + // Avoid doing the long population work when the snapshot is already behind + // the active chainstate. ActivateSnapshot repeats this check before the swap + // in case the active tip advances while the snapshot is being loaded. + if (WITH_LOCK(::cs_main, return !CBlockIndexWorkComparator()(ActiveTip(), snapshot_start_block))) { + LogPrintf("[snapshot] activation failed - height does not exceed active chainstate\n"); + return false; + } + COutPoint outpoint; Coin coin; const uint64_t coins_count = metadata.m_coins_count; @@ -6324,7 +6372,7 @@ SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation( CCoinsViewDB& ibd_coins_db = m_ibd_chainstate->CoinsDB(); - auto maybe_au_data = ExpectedAssumeutxo(curr_height, ::Params()); + auto maybe_au_data = GetParams().AssumeutxoForHeight(curr_height); if (!maybe_au_data) { LogPrintf("[snapshot] assumeutxo data not found for height " /* Continued */ "(%d) - refusing to validate snapshot\n", curr_height); @@ -6637,29 +6685,35 @@ bool ChainstateManager::DetectSnapshotChainstate(CTxMemPool* mempool, bilingual_ LogPrintf("[snapshot] detected active snapshot chainstate (%s) - loading\n", fs::PathToString(*path)); - if (!this->ActivateExistingSnapshot(mempool, *base_blockhash)) { + CEvoDB& evo_db = this->ActiveChainstate().m_evoDb; + uint256 snapshot_evo_tip; + if (!evo_db.ReadBestBlock(EvoDbIdentity::SNAPSHOT, snapshot_evo_tip)) { + LogPrintf("[snapshot] snapshot EvoDB marker is missing for base block %s\n", + base_blockhash->ToString()); error = _("Snapshot chainstate EvoDB marker is missing. Reindex is required."); return false; } + Assert(this->ActiveChainstate().m_mempool == mempool); + this->ActivateExistingSnapshot(*base_blockhash); return true; } -Chainstate* ChainstateManager::ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) +Chainstate& ChainstateManager::ActivateExistingSnapshot(uint256 base_blockhash) { assert(!m_snapshot_chainstate); CEvoDB& evo_db = this->ActiveChainstate().m_evoDb; - uint256 snapshot_evo_tip; - if (!evo_db.ReadBestBlock(EvoDbIdentity::SNAPSHOT, snapshot_evo_tip)) { - LogPrintf("[snapshot] snapshot EvoDB marker is missing for base block %s\n", - base_blockhash.ToString()); - return nullptr; - } m_snapshot_chainstate = std::make_unique( - mempool, m_blockman, *this, + /*mempool=*/nullptr, m_blockman, *this, evo_db, this->ActiveChainstate().m_chain_helper, base_blockhash); LogPrintf("[snapshot] switching active chainstate to %s\n", m_snapshot_chainstate->ToString()); + + Assert(m_active_chainstate->m_mempool); + Assert(m_active_chainstate->m_mempool->size() == 0); + Assert(!m_snapshot_chainstate->m_mempool); + m_snapshot_chainstate->m_mempool = m_active_chainstate->m_mempool; + m_active_chainstate->m_mempool = nullptr; m_active_chainstate = m_snapshot_chainstate.get(); // Only the active chainstate keeps the mempool: the background chainstate // connects historical blocks and must not touch mempool state built on the @@ -6668,17 +6722,22 @@ Chainstate* ChainstateManager::ActivateExistingSnapshot(CTxMemPool* mempool, uin m_ibd_chainstate->m_mempool = nullptr; } evo_db.SetDefaultIdentity(EvoDbIdentity::SNAPSHOT); - return m_snapshot_chainstate.get(); + return *m_snapshot_chainstate; } -util::Result Chainstate::InvalidateCoinsDBOnDisk() +static fs::path GetSnapshotCoinsDBPath(Chainstate& cs) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); // Should never be called on a non-snapshot chainstate. - assert(m_from_snapshot_blockhash); - auto storage_path_maybe = this->CoinsDB().StoragePath(); + assert(cs.m_from_snapshot_blockhash); + auto storage_path_maybe = cs.CoinsDB().StoragePath(); // Should never be called with a non-existent storage path. assert(storage_path_maybe); - const fs::path& snapshot_datadir = *storage_path_maybe; + return *storage_path_maybe; +} + +util::Result Chainstate::InvalidateCoinsDBOnDisk() +{ + fs::path snapshot_datadir = GetSnapshotCoinsDBPath(*this); // Coins views no longer usable. m_coins_views.reset(); @@ -6710,6 +6769,33 @@ util::Result Chainstate::InvalidateCoinsDBOnDisk() return {}; } +bool ChainstateManager::DeleteSnapshotChainstate() +{ + AssertLockHeld(::cs_main); + Assert(m_snapshot_chainstate); + Assert(m_ibd_chainstate); + + fs::path snapshot_datadir = GetSnapshotCoinsDBPath(*m_snapshot_chainstate); + if (!DeleteCoinsDBFromDisk(snapshot_datadir, /*is_snapshot=*/ true)) { + LogPrintf("Deletion of %s failed. Please remove it manually to continue reindexing.\n", + fs::PathToString(snapshot_datadir)); + return false; + } + m_active_chainstate = m_ibd_chainstate.get(); + m_snapshot_chainstate.reset(); + return true; +} + +ChainstateRole Chainstate::GetRole() const +{ + if (m_chainman.GetAll().size() <= 1) { + return ChainstateRole::NORMAL; + } + return (this != &m_chainman.ActiveChainstate()) ? + ChainstateRole::BACKGROUND : + ChainstateRole::ASSUMEDVALID; +} + const CBlockIndex* ChainstateManager::GetSnapshotBaseBlock() const { return m_active_chainstate ? m_active_chainstate->SnapshotBase() : nullptr; @@ -6828,3 +6914,38 @@ ChainstateRole Chainstate::GetRole() const ChainstateRole::BACKGROUND : ChainstateRole::ASSUMEDVALID; } + +Chainstate& ChainstateManager::GetChainstateForIndexing() +{ + // We can't always return `m_ibd_chainstate` because after background validation + // has completed, `m_snapshot_chainstate == m_active_chainstate`, but it can be + // indexed. + return (this->GetAll().size() > 1) ? *m_ibd_chainstate : *m_active_chainstate; +} + +std::pair ChainstateManager::GetPruneRange(const Chainstate& chainstate, int last_height_can_prune) +{ + if (chainstate.m_chain.Height() <= 0) { + return {0, 0}; + } + int prune_start{0}; + + if (this->GetAll().size() > 1 && m_snapshot_chainstate.get() == &chainstate) { + // Leave the blocks in the background IBD chain alone if we're pruning + // the snapshot chain. + prune_start = *Assert(GetSnapshotBaseHeight()) + 1; + } + + int max_prune = std::max( + 0, chainstate.m_chain.Height() - static_cast(MIN_BLOCKS_TO_KEEP)); + + // last block to prune is the lesser of (caller-specified height, MIN_BLOCKS_TO_KEEP from the tip) + // + // While you might be tempted to prune the background chainstate more + // aggressively (i.e. fewer MIN_BLOCKS_TO_KEEP), this won't work with index + // building - specifically blockfilterindex requires undo data, and if + // we don't maintain this trailing window, we hit indexing failures. + int prune_end = std::min(last_height_can_prune, max_prune); + + return {prune_start, prune_end}; +} diff --git a/src/validation.h b/src/validation.h index 28ef3bf65afd..424529ffda10 100644 --- a/src/validation.h +++ b/src/validation.h @@ -540,17 +540,17 @@ class Chainstate const std::unique_ptr& chain_helper, std::optional from_snapshot_blockhash = std::nullopt); + //! Return the stable EvoDB identity corresponding to this chainstate's coins DB. + ::EvoDbIdentity EvoDbIdentity() const; + + std::string EvoDbInconsistencyMessage(); + //! Return the current role of the chainstate. See `ChainstateManager` //! documentation for a description of the different types of chainstates. //! //! @sa ChainstateRole ChainstateRole GetRole() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - //! Return the stable EvoDB identity corresponding to this chainstate's coins DB. - ::EvoDbIdentity EvoDbIdentity() const; - - std::string EvoDbInconsistencyMessage(); - /** * Initialize the CoinsViews UTXO set database management data structures. The in-memory * cache is initialized separately. @@ -916,9 +916,6 @@ class ChainstateManager //! Points to either the ibd or snapshot chainstate; indicates our //! most-work chain. //! - //! Once this pointer is set to a corresponding chainstate, it will not - //! be reset until init.cpp:Shutdown(). - //! //! This is especially important when, e.g., calling ActivateBestChain() //! on all chainstates because we are not able to hold ::cs_main going into //! that call. @@ -945,13 +942,6 @@ class ChainstateManager std::array m_warningcache GUARDED_BY(::cs_main); - //! Returns nullptr if no snapshot has been loaded. - const CBlockIndex* GetSnapshotBaseBlock() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - - //! Return the height of the base block of the snapshot in use, if one exists, else - //! nullopt. - std::optional GetSnapshotBaseHeight() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - //! Return true if a chainstate is considered usable. //! //! This is false when a background validation chainstate has completed its @@ -968,6 +958,10 @@ class ChainstateManager : m_options{std::move(options)}, m_blockman{{m_options.chainparams}} {} + //! Function to restart active indexes; set dynamically to avoid a circular + //! dependency on index/base.cpp. + std::function restart_indexes = std::function{}; + const CChainParams& GetParams() const { return m_options.chainparams; } const Consensus::Params& GetConsensus() const { return m_options.chainparams.GetConsensus(); } @@ -1098,12 +1092,25 @@ class ChainstateManager std::function shutdown_fnc = [](bilingual_str msg) { AbortNode(msg.original, msg); }); + //! Returns nullptr if no snapshot has been loaded. + const CBlockIndex* GetSnapshotBaseBlock() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! The most-work chain. Chainstate& ActiveChainstate() const; CChain& ActiveChain() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChainstate().m_chain; } int ActiveHeight() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChain().Height(); } CBlockIndex* ActiveTip() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChain().Tip(); } + //! The state of a background sync (for net processing) + bool BackgroundSyncInProgress() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { + return IsUsable(m_snapshot_chainstate.get()) && IsUsable(m_ibd_chainstate.get()); + } + + //! The tip of the background sync chain + const CBlockIndex* GetBackgroundSyncTip() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { + return BackgroundSyncInProgress() ? m_ibd_chainstate->m_chain.Tip() : nullptr; + } + node::BlockMap& BlockIndex() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); @@ -1259,8 +1266,13 @@ class ChainstateManager //! Switch the active chainstate to one based on a UTXO snapshot that was loaded //! previously. - Chainstate* ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) - EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Remove the snapshot-based chainstate and all on-disk artifacts. + //! Used when reindex{-chainstate} is called during snapshot use. + [[nodiscard]] bool DeleteSnapshotChainstate() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + + //! Switch the active chainstate to one based on a UTXO snapshot that was loaded + //! previously. + Chainstate& ActivateExistingSnapshot(uint256 base_blockhash) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); //! If we have validated a snapshot chain during this runtime, copy its //! chainstate directory over to the main `chainstate` location, completing @@ -1273,6 +1285,26 @@ class ChainstateManager //! @sa node/chainstate:LoadChainstate() bool ValidatedSnapshotCleanup() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! @returns the chainstate that indexes should consult when ensuring that an + //! index is synced with a chain where we can expect block index entries to have + //! BLOCK_HAVE_DATA beneath the tip. + //! + //! In other words, give us the chainstate for which we can reasonably expect + //! that all blocks beneath the tip have been indexed. In practice this means + //! when using an assumed-valid chainstate based upon a snapshot, return only the + //! fully validated chain. + Chainstate& GetChainstateForIndexing() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + + //! Return the [start, end] (inclusive) of block heights we can prune. + //! + //! start > end is possible, meaning no blocks can be pruned. + std::pair GetPruneRange( + const Chainstate& chainstate, int last_height_can_prune) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + + //! Return the height of the base block of the snapshot in use, if one exists, else + //! nullopt. + std::optional GetSnapshotBaseHeight() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + ~ChainstateManager(); }; @@ -1306,15 +1338,6 @@ bool DeploymentEnabled(const ChainstateManager& chainman, DEP dep) /** Determine the masternode reward era for the block following pindexPrev. */ MnRewardEra GetMnRewardEraAfter(const CBlockIndex* pindexPrev, const ChainstateManager& chainman); -/** - * Return the expected assumeutxo value for a given height, if one exists. - * - * @param[in] height Get the assumeutxo value for this height. - * - * @returns empty if no assumeutxo configuration exists for the given height. - */ -const AssumeutxoData* ExpectedAssumeutxo(const int height, const CChainParams& params); - /** * Remove a persisted snapshot chainstate's on-disk artifacts: its coins database * and the base-blockhash file identifying it. Only valid while no snapshot diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp index 496be65c8d29..eeea283b5011 100644 --- a/src/validationinterface.cpp +++ b/src/validationinterface.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -229,9 +230,9 @@ void CMainSignals::TransactionRemovedFromMempool(const CTransactionRef& tx, MemP RemovalReasonToString(reason)); } -void CMainSignals::BlockConnected(const std::shared_ptr &pblock, const CBlockIndex *pindex) { - auto event = [pblock, pindex, this] { - m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockConnected(pblock, pindex); }); +void CMainSignals::BlockConnected(ChainstateRole role, const std::shared_ptr &pblock, const CBlockIndex *pindex) { + auto event = [role, pblock, pindex, this] { + m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockConnected(role, pblock, pindex); }); }; ENQUEUE_AND_LOG_EVENT(event, "%s: block hash=%s block height=%d", __func__, pblock->GetHash().ToString(), @@ -247,9 +248,9 @@ void CMainSignals::BlockDisconnected(const std::shared_ptr &pblock pindex->nHeight); } -void CMainSignals::ChainStateFlushed(const CBlockLocator &locator) { - auto event = [locator, this] { - m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.ChainStateFlushed(locator); }); +void CMainSignals::ChainStateFlushed(ChainstateRole role, const CBlockLocator &locator) { + auto event = [role, locator, this] { + m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.ChainStateFlushed(role, locator); }); }; ENQUEUE_AND_LOG_EVENT(event, "%s: block hash=%s", __func__, locator.IsNull() ? "null" : locator.vHave.front().ToString()); diff --git a/src/validationinterface.h b/src/validationinterface.h index ad41b0d6c729..3ab9834f3fab 100644 --- a/src/validationinterface.h +++ b/src/validationinterface.h @@ -6,6 +6,7 @@ #ifndef BITCOIN_VALIDATIONINTERFACE_H #define BITCOIN_VALIDATIONINTERFACE_H +#include #include // CTransaction(Ref) #include @@ -104,7 +105,7 @@ class CValidationInterface { * but may not be called on every intermediate tip. If the latter behavior is desired, * subscribe to BlockConnected() instead. * - * Called on a background thread. + * Called on a background thread. Only called for the active chainstate. */ virtual void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) {} /** @@ -168,11 +169,12 @@ class CValidationInterface { * * Called on a background thread. */ - virtual void BlockConnected(const std::shared_ptr &block, const CBlockIndex *pindex) {} + virtual void BlockConnected(ChainstateRole role, const std::shared_ptr &block, const CBlockIndex *pindex) {} /** * Notifies listeners of a block being disconnected * - * Called on a background thread. + * Called on a background thread. Only called for the active chainstate, since + * background chainstates should never disconnect blocks. */ virtual void BlockDisconnected(const std::shared_ptr &block, const CBlockIndex *pindex) {} virtual void NotifyTransactionLock(const CTransactionRef &tx, const std::shared_ptr& islock) {} @@ -198,17 +200,18 @@ class CValidationInterface { * * Called on a background thread. */ - virtual void ChainStateFlushed(const CBlockLocator &locator) {} + virtual void ChainStateFlushed(ChainstateRole role, const CBlockLocator &locator) {} /** * Notifies listeners of a block validation result. * If the provided BlockValidationState IsValid, the provided block * is guaranteed to be the current best block at the time the - * callback was generated (not necessarily now) + * callback was generated (not necessarily now). */ virtual void BlockChecked(const CBlock&, const BlockValidationState&) {} /** * Notifies listeners that a block which builds directly on our current tip - * has been received and connected to the headers tree, though not validated yet */ + * has been received and connected to the headers tree, though not validated yet. + */ virtual void NewPoWValidBlock(const CBlockIndex *pindex, const std::shared_ptr& block) {}; friend class CMainSignals; friend class ValidationInterfaceTest; @@ -242,7 +245,7 @@ class CMainSignals { void SynchronousUpdatedBlockTip(const CBlockIndex *, const CBlockIndex *, bool fInitialDownload); void TransactionAddedToMempool(const CTransactionRef&, int64_t, uint64_t mempool_sequence); void TransactionRemovedFromMempool(const CTransactionRef&, MemPoolRemovalReason, uint64_t mempool_sequence); - void BlockConnected(const std::shared_ptr &, const CBlockIndex *pindex); + void BlockConnected(ChainstateRole, const std::shared_ptr &, const CBlockIndex *pindex); void BlockDisconnected(const std::shared_ptr &, const CBlockIndex* pindex); void NotifyTransactionLock(const CTransactionRef &tx, const std::shared_ptr& islock); void NotifyChainLock(const CBlockIndex* pindex, const std::shared_ptr& clsig, const std::string& id); @@ -251,7 +254,7 @@ class CMainSignals { void NotifyInstantSendDoubleSpendAttempt(const CTransactionRef ¤tTx, const CTransactionRef &previousTx); void NotifyRecoveredSig(const std::shared_ptr &sig, const std::string& id, bool proactive_relay); void NotifyMasternodeListChanged(bool undo, const CDeterministicMNList& oldMNList, const CDeterministicMNListDiff& diff); - void ChainStateFlushed(const CBlockLocator &); + void ChainStateFlushed(ChainstateRole, const CBlockLocator &); void BlockChecked(const CBlock&, const BlockValidationState&); void NewPoWValidBlock(const CBlockIndex *, const std::shared_ptr&); }; diff --git a/src/wallet/test/fuzz/notifications.cpp b/src/wallet/test/fuzz/notifications.cpp index 1f20ba463cba..43db22fee9c6 100644 --- a/src/wallet/test/fuzz/notifications.cpp +++ b/src/wallet/test/fuzz/notifications.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include #include @@ -140,8 +141,9 @@ FUZZ_TARGET(wallet_notifications, .init = initialize_setup) info.prev_hash = &block.hashPrevBlock; info.height = chain.size(); info.data = █ - a.wallet->blockConnected(info); - b.wallet->blockConnected(info); + info.chain_time_max = std::numeric_limits::max(); + a.wallet->blockConnected(ChainstateRole::NORMAL, info); + b.wallet->blockConnected(ChainstateRole::NORMAL, info); // Store the coins for the next block Coins coins_new; for (const auto& tx : block.vtx) { diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index da4a2e08d885..987bd846f483 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -629,11 +630,11 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, return false; } -void CWallet::chainStateFlushed(const CBlockLocator& loc) +void CWallet::chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) { // Don't update the best block until the chain is attached so that in case of a shutdown, // the rescan will be restarted at next startup. - if (m_attaching_chain) { + if (m_attaching_chain || role == ChainstateRole::BACKGROUND) { return; } WalletBatch batch(GetDatabase()); @@ -1475,8 +1476,11 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe } } -void CWallet::blockConnected(const interfaces::BlockInfo& block) +void CWallet::blockConnected(ChainstateRole role, const interfaces::BlockInfo& block) { + if (role == ChainstateRole::BACKGROUND) { + return; + } assert(block.data); LOCK(cs_wallet); @@ -3245,7 +3249,7 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri } if (chain) { - walletInstance->chainStateFlushed(chain->getTipLocator()); + walletInstance->chainStateFlushed(ChainstateRole::NORMAL, chain->getTipLocator()); } // Try to create wallet backup right after new wallet was created @@ -3549,7 +3553,7 @@ bool CWallet::AttachChain(const std::shared_ptr& walletInstance, interf } } walletInstance->m_attaching_chain = false; - walletInstance->chainStateFlushed(chain.getTipLocator()); + walletInstance->chainStateFlushed(ChainstateRole::NORMAL, chain.getTipLocator()); walletInstance->GetDatabase().IncrementUpdateCounter(); } walletInstance->m_attaching_chain = false; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 641065321e1e..c7d75e5d4200 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -723,7 +723,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati CWalletTx* AddToWallet(CTransactionRef tx, const TxState& state, const UpdateWalletTxFn& update_wtx=nullptr, bool fFlushOnClose=true, bool rescanning_old_block = false); bool LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void transactionAddedToMempool(const CTransactionRef& tx, int64_t nAcceptTime) override; - void blockConnected(const interfaces::BlockInfo& block) override; + void blockConnected(ChainstateRole role, const interfaces::BlockInfo& block) override; void blockDisconnected(const interfaces::BlockInfo& block) override; void updatedBlockTip() override; int64_t RescanFromTime(int64_t startTime, const WalletRescanReserver& reserver, bool update); @@ -912,7 +912,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati /** should probably be renamed to IsRelevantToMe */ bool IsFromMe(const CTransaction& tx) const; CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const; - void chainStateFlushed(const CBlockLocator& loc) override; + void chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) override; DBErrors LoadWallet(); void AutoLockMasternodeCollaterals(); diff --git a/src/zmq/zmqnotificationinterface.cpp b/src/zmq/zmqnotificationinterface.cpp index 904e9dca9f27..3ba1651f4296 100644 --- a/src/zmq/zmqnotificationinterface.cpp +++ b/src/zmq/zmqnotificationinterface.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -193,8 +194,11 @@ void CZMQNotificationInterface::TransactionRemovedFromMempool(const CTransaction }); } -void CZMQNotificationInterface::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) +void CZMQNotificationInterface::BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) { + if (role == ChainstateRole::BACKGROUND) { + return; + } for (const CTransactionRef& ptx : pblock->vtx) { const CTransaction& tx = *ptx; TryForEachAndRemoveFailed(notifiers, [&tx](CZMQAbstractNotifier* notifier) { diff --git a/src/zmq/zmqnotificationinterface.h b/src/zmq/zmqnotificationinterface.h index f5800174c8ec..e18581245364 100644 --- a/src/zmq/zmqnotificationinterface.h +++ b/src/zmq/zmqnotificationinterface.h @@ -32,7 +32,7 @@ class CZMQNotificationInterface final : public CValidationInterface // CValidationInterface void TransactionAddedToMempool(const CTransactionRef& tx, int64_t nAcceptTime, uint64_t mempool_sequence) override; void TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t mempool_sequence) override; - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) override; + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) override; void BlockDisconnected(const std::shared_ptr& pblock, const CBlockIndex* pindexDisconnected) override; void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) override; void NotifyChainLock(const CBlockIndex *pindex, const std::shared_ptr& clsig) override; diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py new file mode 100755 index 000000000000..be1aa1899380 --- /dev/null +++ b/test/functional/feature_assumeutxo.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test for assumeutxo, a means of quickly bootstrapping a node using +a serialized version of the UTXO set at a certain height, which corresponds +to a hash that has been compiled into bitcoind. + +The assumeutxo value generated and used here is committed to in +`CRegTestParams::m_assumeutxo_data` in `src/chainparams.cpp`. + +## Possible test improvements + +- TODO: test submitting a transaction and verifying it appears in mempool +- TODO: test what happens with -reindex and -reindex-chainstate before the + snapshot is validated, and make sure it's deleted successfully. + +Interesting test cases could be loading an assumeutxo snapshot file with: + +- TODO: An invalid hash +- TODO: Valid hash but invalid snapshot file (bad coin height or truncated file or + bad other serialization) +- TODO: Valid snapshot file, but referencing an unknown block +- TODO: Valid snapshot file, but referencing a snapshot block that turns out to be + invalid, or has an invalid parent +- TODO: Valid snapshot file and snapshot block, but the block is not on the + most-work chain + +Interesting starting states could be loading a snapshot when the current chain tip is: + +- TODO: An ancestor of snapshot block +- TODO: Not an ancestor of the snapshot block but has less work +- TODO: The snapshot block +- TODO: A descendant of the snapshot block +- TODO: Not an ancestor or a descendant of the snapshot block and has more work + +""" +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, wait_until_helper + +START_HEIGHT = 199 +SNAPSHOT_BASE_HEIGHT = 299 +FINAL_HEIGHT = 399 +COMPLETE_IDX = {'synced': True, 'best_block_height': FINAL_HEIGHT} + + +class AssumeutxoTest(BitcoinTestFramework): + + def set_test_params(self): + """Use the pregenerated, deterministic chain up to height 199.""" + self.num_nodes = 3 + self.rpc_timeout = 120 + self.extra_args = [ + [], + ["-fastprune", "-prune=1", "-blockfilterindex=1", "-coinstatsindex=1"], + ["-txindex=1", "-blockfilterindex=1", "-coinstatsindex=1"], + ] + + def setup_network(self): + """Start with the nodes disconnected so that one can generate a snapshot + including blocks the other hasn't yet seen.""" + self.add_nodes(3) + self.start_nodes(extra_args=self.extra_args) + + def run_test(self): + """ + Bring up two (disconnected) nodes, mine some new blocks on the first, + and generate a UTXO snapshot. + + Load the snapshot into the second, ensure it syncs to tip and completes + background validation when connected to the first. + """ + n0 = self.nodes[0] + n1 = self.nodes[1] + n2 = self.nodes[2] + + # Mock time for a deterministic chain + for n in self.nodes: + n.setmocktime(n.getblockheader(n.getbestblockhash())['time']) + + self.sync_blocks() + + def no_sync(): + pass + + # Generate a series of blocks that `n0` will have in the snapshot, + # but that n1 doesn't yet see. In order for the snapshot to activate, + # though, we have to ferry over the new headers to n1 so that it + # isn't waiting forever to see the header of the snapshot's base block + # while disconnected from n0. + for i in range(100): + self.generate(n0, nblocks=1, sync_fun=no_sync) + newblock = n0.getblock(n0.getbestblockhash(), 0) + + # make n1 aware of the new header, but don't give it the block. + n1.submitheader(newblock) + n2.submitheader(newblock) + + # Ensure everyone is seeing the same headers. + for n in self.nodes: + assert_equal(n.getblockchaininfo()["headers"], SNAPSHOT_BASE_HEIGHT) + + self.log.info("-- Testing assumeutxo + some indexes + pruning") + + assert_equal(n0.getblockcount(), SNAPSHOT_BASE_HEIGHT) + assert_equal(n1.getblockcount(), START_HEIGHT) + + self.log.info(f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}") + dump_output = n0.dumptxoutset('utxos.dat') + + assert_equal( + dump_output['txoutset_hash'], + 'ef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0') + assert_equal(dump_output['nchaintx'], 300) + assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) + + # Mine more blocks on top of the snapshot that n1 hasn't yet seen. This + # will allow us to test n1's sync-to-tip on top of a snapshot. + self.generate(n0, nblocks=100, sync_fun=no_sync) + + assert_equal(n0.getblockcount(), FINAL_HEIGHT) + assert_equal(n1.getblockcount(), START_HEIGHT) + + assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT) + + self.log.info(f"Loading snapshot into second node from {dump_output['path']}") + loaded = n1.loadtxoutset(dump_output['path']) + assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) + assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT) + + monitor = n1.getchainstates() + assert_equal(monitor['normal']['blocks'], START_HEIGHT) + assert_equal(monitor['snapshot']['blocks'], SNAPSHOT_BASE_HEIGHT) + assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash']) + + assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) + + PAUSE_HEIGHT = FINAL_HEIGHT - 40 + + self.log.info("Restarting node to stop at height %d", PAUSE_HEIGHT) + self.restart_node(1, extra_args=[ + f"-stopatheight={PAUSE_HEIGHT}", *self.extra_args[1]]) + + # Finally connect the nodes and let them sync. + self.connect_nodes(0, 1) + + n1.wait_until_stopped(timeout=5) + + self.log.info("Checking that blocks are segmented on disk") + assert self.has_blockfile(n1, "00000"), "normal blockfile missing" + assert self.has_blockfile(n1, "00001"), "assumed blockfile missing" + assert not self.has_blockfile(n1, "00002"), "too many blockfiles" + + self.log.info("Restarted node before snapshot validation completed, reloading...") + self.restart_node(1, extra_args=self.extra_args[1]) + self.connect_nodes(0, 1) + + self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})") + wait_until_helper(lambda: n1.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) + self.sync_blocks(nodes=(n0, n1)) + + self.log.info("Ensuring background validation completes") + # N.B.: the `snapshot` key disappears once the background validation is complete. + wait_until_helper(lambda: not n1.getchainstates().get('snapshot')) + + # Ensure indexes have synced. + completed_idx_state = { + 'basic block filter index': COMPLETE_IDX, + 'coinstatsindex': COMPLETE_IDX, + } + self.wait_until(lambda: n1.getindexinfo() == completed_idx_state) + + + for i in (0, 1): + n = self.nodes[i] + self.log.info(f"Restarting node {i} to ensure (Check|Load)BlockIndex passes") + self.restart_node(i, extra_args=self.extra_args[i]) + + assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT) + + assert_equal(n.getchainstates()['normal']['blocks'], FINAL_HEIGHT) + assert_equal(n.getchainstates().get('snapshot'), None) + + if i != 0: + # Ensure indexes have synced for the assumeutxo node + self.wait_until(lambda: n.getindexinfo() == completed_idx_state) + + + # Node 2: all indexes + reindex + # ----------------------------- + + self.log.info("-- Testing all indexes + reindex") + assert_equal(n2.getblockcount(), START_HEIGHT) + + self.log.info(f"Loading snapshot into third node from {dump_output['path']}") + loaded = n2.loadtxoutset(dump_output['path']) + assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) + assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT) + + monitor = n2.getchainstates() + assert_equal(monitor['normal']['blocks'], START_HEIGHT) + assert_equal(monitor['snapshot']['blocks'], SNAPSHOT_BASE_HEIGHT) + assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash']) + + self.connect_nodes(0, 2) + wait_until_helper(lambda: n2.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) + self.sync_blocks() + + self.log.info("Ensuring background validation completes") + wait_until_helper(lambda: not n2.getchainstates().get('snapshot')) + + completed_idx_state = { + 'basic block filter index': COMPLETE_IDX, + 'coinstatsindex': COMPLETE_IDX, + 'txindex': COMPLETE_IDX, + } + self.wait_until(lambda: n2.getindexinfo() == completed_idx_state) + + for i in (0, 2): + n = self.nodes[i] + self.log.info(f"Restarting node {i} to ensure (Check|Load)BlockIndex passes") + self.restart_node(i, extra_args=self.extra_args[i]) + + assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT) + + assert_equal(n.getchainstates()['normal']['blocks'], FINAL_HEIGHT) + assert_equal(n.getchainstates().get('snapshot'), None) + + if i != 0: + # Ensure indexes have synced for the assumeutxo node + self.wait_until(lambda: n.getindexinfo() == completed_idx_state) + + self.log.info("Test -reindex-chainstate of an assumeutxo-synced node") + self.restart_node(2, extra_args=[ + '-reindex-chainstate=1', *self.extra_args[2]]) + assert_equal(n2.getblockchaininfo()["blocks"], FINAL_HEIGHT) + wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT) + + self.log.info("Test -reindex of an assumeutxo-synced node") + self.restart_node(2, extra_args=['-reindex=1', *self.extra_args[2]]) + self.connect_nodes(0, 2) + wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT) + + +if __name__ == '__main__': + AssumeutxoTest().main() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index eb00105ca1b8..3d342402ec8d 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -1163,6 +1163,10 @@ def is_bdb_compiled(self): """Checks whether the wallet module was compiled with BDB support.""" return self.config["components"].getboolean("USE_BDB") + def has_blockfile(self, node, filenum: str): + blocksdir = os.path.join(node.datadir, self.chain, 'blocks', '') + return os.path.isfile(os.path.join(blocksdir, f"blk{filenum}.dat")) + MASTERNODE_COLLATERAL = 1000 EVONODE_COLLATERAL = 4000 diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 88c12d0a225e..6b8e1095c187 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -379,6 +379,7 @@ 'wallet_coinbase_category.py --descriptors', 'feature_filelock.py', 'feature_loadblock.py', + 'feature_assumeutxo.py', 'p2p_dos_header_tree.py', 'p2p_add_connections.py', 'feature_bind_port_discover.py', diff --git a/test/lint/lint-shell.py b/test/lint/lint-shell.py index 4be1f297fa11..a9ece3200677 100755 --- a/test/lint/lint-shell.py +++ b/test/lint/lint-shell.py @@ -67,9 +67,13 @@ def main(): '*.sh', ] files = get_files(files_cmd) - # remove everything that doesn't match this regex reg = re.compile(r'src/[dashbls,immer,leveldb,secp256k1,minisketch]') - files[:] = [file for file in files if not reg.match(file)] + + def should_exclude(fname: str) -> bool: + return bool(reg.match(fname)) or 'test_utxo_snapshots.sh' in fname + + # remove everything that doesn't match this regex + files[:] = [file for file in files if not should_exclude(file)] # build the `shellcheck` command shellcheck_cmd = [ From 399a9dfb6e8cf045e65dcc29ad661139670ef15c Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 13:48:32 -0500 Subject: [PATCH 03/30] backport: adapt Dash for bitcoin#27596 --- contrib/devtools/test_utxo_snapshots.sh | 29 +++++++++-------- doc/release-notes-27596.md | 4 +-- src/chainlock/handler.cpp | 3 +- src/chainlock/handler.h | 2 +- src/chainlock/signing.cpp | 3 +- src/chainlock/signing.h | 2 +- src/chainparams.cpp | 14 +++++---- src/dsnotificationinterface.cpp | 3 +- src/dsnotificationinterface.h | 2 +- src/index/base.cpp | 10 +++--- src/index/base.h | 6 ++-- src/instantsend/net_instantsend.cpp | 17 +++++----- src/instantsend/net_instantsend.h | 7 +++-- src/interfaces/chain.h | 15 +++++++++ src/node/blockstorage.cpp | 8 +++-- src/node/context.h | 2 ++ src/rpc/blockchain.cpp | 17 +++++++--- src/validation.cpp | 41 ++++++++++++++----------- src/validation.h | 6 ++-- test/lint/lint-circular-dependencies.py | 1 + 20 files changed, 119 insertions(+), 73 deletions(-) diff --git a/contrib/devtools/test_utxo_snapshots.sh b/contrib/devtools/test_utxo_snapshots.sh index d4c49bf098f2..3d1da706e40e 100755 --- a/contrib/devtools/test_utxo_snapshots.sh +++ b/contrib/devtools/test_utxo_snapshots.sh @@ -23,7 +23,7 @@ SERVER_DATADIR="$(pwd)/utxodemo-data-server-$BASE_HEIGHT" CLIENT_DATADIR="$(pwd)/utxodemo-data-client-$BASE_HEIGHT" UTXO_DAT_FILE="$(pwd)/utxo.$BASE_HEIGHT.dat" -# Chosen to try to not interfere with any running bitcoind processes. +# Chosen to try to not interfere with any running dashd processes. SERVER_PORT=8633 SERVER_RPC_PORT=8632 @@ -61,10 +61,10 @@ trap finish EXIT EARLY_IBD_FLAGS="-maxtipage=9223372036854775207 -minimumchainwork=0x00" server_rpc() { - ./src/bitcoin-cli -rpcport=$SERVER_RPC_PORT -datadir="$SERVER_DATADIR" "$@" + ./src/dash-cli -rpcport=$SERVER_RPC_PORT -datadir="$SERVER_DATADIR" "$@" } client_rpc() { - ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir="$CLIENT_DATADIR" "$@" + ./src/dash-cli -rpcport=$CLIENT_RPC_PORT -datadir="$CLIENT_DATADIR" "$@" } server_sleep_til_boot() { while ! server_rpc ping >/dev/null 2>&1; do sleep 0.1; done @@ -104,13 +104,13 @@ read -p "Press [enter] to continue" _ echo echo "-- IBDing the blocks (height=$BASE_HEIGHT) required to the server node..." -./src/bitcoind -logthreadnames=1 $SERVER_PORTS \ +./src/dashd -logthreadnames=1 $SERVER_PORTS \ -datadir="$SERVER_DATADIR" $EARLY_IBD_FLAGS -stopatheight="$BASE_HEIGHT" >/dev/null echo echo "-- Creating snapshot at ~ height $BASE_HEIGHT ($UTXO_DAT_FILE)..." sleep 2 -./src/bitcoind -logthreadnames=1 $SERVER_PORTS \ +./src/dashd -logthreadnames=1 $SERVER_PORTS \ -datadir="$SERVER_DATADIR" $EARLY_IBD_FLAGS -connect=0 -listen=0 >/dev/null & SERVER_PID="$!" @@ -121,6 +121,7 @@ kill -9 "$SERVER_PID" RPC_BASE_HEIGHT=$(jq -r .base_height < "$DUMP_OUTPUT") RPC_AU=$(jq -r .txoutset_hash < "$DUMP_OUTPUT") +RPC_EVO=$(jq -r .evo_hash < "$DUMP_OUTPUT") RPC_NCHAINTX=$(jq -r .nchaintx < "$DUMP_OUTPUT") RPC_BLOCKHASH=$(jq -r .base_hash < "$DUMP_OUTPUT") @@ -129,18 +130,20 @@ while server_rpc ping >/dev/null 2>&1; do sleep 0.1; done echo echo "-- Now: add the following to CMainParams::m_assumeutxo_data" -echo " in src/kernel/chainparams.cpp, and recompile:" +echo " in src/chainparams.cpp, and recompile:" echo -echo " {${RPC_BASE_HEIGHT}, AssumeutxoHash{uint256S(\"0x${RPC_AU}\")}, ${RPC_NCHAINTX}, uint256S(\"0x${RPC_BLOCKHASH}\")}," +echo " {.height = ${RPC_BASE_HEIGHT}, .hash_serialized = AssumeutxoHash{uint256S(\"0x${RPC_AU}\")}," +echo " .evo_hash = EvoSnapshotHash{uint256S(\"0x${RPC_EVO}\")}, .nChainTx = ${RPC_NCHAINTX}," +echo " .blockhash = uint256S(\"0x${RPC_BLOCKHASH}\")}," echo echo echo "-- IBDing more blocks to the server node (height=$FINAL_HEIGHT) so there is a diff between snapshot and tip..." -./src/bitcoind $SERVER_PORTS -logthreadnames=1 -datadir="$SERVER_DATADIR" \ +./src/dashd $SERVER_PORTS -logthreadnames=1 -datadir="$SERVER_DATADIR" \ $EARLY_IBD_FLAGS -stopatheight="$FINAL_HEIGHT" >/dev/null echo echo "-- Starting the server node to provide blocks to the client node..." -./src/bitcoind $SERVER_PORTS -logthreadnames=1 -debug=net -datadir="$SERVER_DATADIR" \ +./src/dashd $SERVER_PORTS -logthreadnames=1 -debug=net -datadir="$SERVER_DATADIR" \ $EARLY_IBD_FLAGS -connect=0 -listen=1 >/dev/null & SERVER_PID="$!" server_sleep_til_boot @@ -163,7 +166,7 @@ read -p "When you're ready for all this, hit [enter]" _ echo echo "-- Starting the client node to get headers from the server, then load the snapshot..." -./src/bitcoind $CLIENT_PORTS $ALL_INDEXES -logthreadnames=1 -datadir="$CLIENT_DATADIR" \ +./src/dashd $CLIENT_PORTS $ALL_INDEXES -logthreadnames=1 -datadir="$CLIENT_DATADIR" \ -connect=0 -addnode=127.0.0.1:$SERVER_PORT -debug=net $EARLY_IBD_FLAGS >/dev/null & CLIENT_PID="$!" client_sleep_til_boot @@ -176,7 +179,7 @@ echo echo "-- Loading UTXO snapshot into client..." client_rpc loadtxoutset "$UTXO_DAT_FILE" -watch -n 0.3 "( tail -n 14 $CLIENT_DATADIR/debug.log ; echo ; ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat" +watch -n 0.3 "( tail -n 14 $CLIENT_DATADIR/debug.log ; echo ; ./src/dash-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat" echo echo "-- Okay, now I'm going to restart the client to make sure that the snapshot chain reloads " @@ -189,12 +192,12 @@ read -p "Press [enter] to continue" while kill -0 "$CLIENT_PID"; do sleep 1 done -./src/bitcoind $CLIENT_PORTS $ALL_INDEXES -logthreadnames=1 -datadir="$CLIENT_DATADIR" -connect=0 \ +./src/dashd $CLIENT_PORTS $ALL_INDEXES -logthreadnames=1 -datadir="$CLIENT_DATADIR" -connect=0 \ -addnode=127.0.0.1:$SERVER_PORT "$EARLY_IBD_FLAGS" >/dev/null & CLIENT_PID="$!" client_sleep_til_boot -watch -n 0.3 "( tail -n 14 $CLIENT_DATADIR/debug.log ; echo ; ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat" +watch -n 0.3 "( tail -n 14 $CLIENT_DATADIR/debug.log ; echo ; ./src/dash-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat" echo echo "-- Done!" diff --git a/doc/release-notes-27596.md b/doc/release-notes-27596.md index 799b82643fec..2789af210dae 100644 --- a/doc/release-notes-27596.md +++ b/doc/release-notes-27596.md @@ -17,12 +17,12 @@ the network's tip under a security model very much like `assumevalid`. Meanwhile, the original chainstate will complete the initial block download process in the background, eventually validating up to the block that the snapshot is based upon. -The result is a usable bitcoind instance that is current with the network tip in a +The result is a usable dashd instance that is current with the network tip in a matter of minutes rather than hours. UTXO snapshot are typically obtained via third-party sources (HTTP, torrent, etc.) which is reasonable since their contents are always checked by hash. You can find more information on this process in the `assumeutxo` design -document (). +document (). `getchainstates` has been added to aid in monitoring the assumeutxo sync process. diff --git a/src/chainlock/handler.cpp b/src/chainlock/handler.cpp index 2201f47a4efe..e57c16753be4 100644 --- a/src/chainlock/handler.cpp +++ b/src/chainlock/handler.cpp @@ -204,8 +204,9 @@ void ChainlockHandler::AcceptedBlockHeader(const CBlockIndex* pindexNew) m_chainlocks.AcceptedBlockHeader(pindexNew); } -void ChainlockHandler::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) +void ChainlockHandler::BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) { + if (role == ChainstateRole::BACKGROUND) return; if (!m_mn_sync.IsBlockchainSynced()) { return; } diff --git a/src/chainlock/handler.h b/src/chainlock/handler.h index d369113eeb4f..7256687845da 100644 --- a/src/chainlock/handler.h +++ b/src/chainlock/handler.h @@ -100,7 +100,7 @@ class ChainlockHandler final : public CValidationInterface void TransactionAddedToMempool(const CTransactionRef& tx, int64_t nAcceptTime, uint64_t mempool_sequence) override EXCLUSIVE_LOCKS_REQUIRED(!cs); - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) override EXCLUSIVE_LOCKS_REQUIRED(!cs); private: diff --git a/src/chainlock/signing.cpp b/src/chainlock/signing.cpp index 3fe4f4e6e413..c04ea6090d83 100644 --- a/src/chainlock/signing.cpp +++ b/src/chainlock/signing.cpp @@ -185,8 +185,9 @@ void ChainLockSigner::BlockDisconnected(const std::shared_ptr& blo } -void ChainLockSigner::BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) +void ChainLockSigner::BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) { + if (role == ChainstateRole::BACKGROUND) return; if (!m_mn_sync.IsBlockchainSynced()) { return; } diff --git a/src/chainlock/signing.h b/src/chainlock/signing.h index b077026fe303..1dafed891ef1 100644 --- a/src/chainlock/signing.h +++ b/src/chainlock/signing.h @@ -72,7 +72,7 @@ class ChainLockSigner final : public llmq::CRecoveredSigsListener, public CValid void UnregisterRecoveryInterface(); // implements validation interface: - void BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) override EXCLUSIVE_LOCKS_REQUIRED(!cs_signer); void BlockDisconnected(const std::shared_ptr& block, const CBlockIndex* pindex) override EXCLUSIVE_LOCKS_REQUIRED(!cs_signer); diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 24228164421c..e36530701d17 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -883,25 +883,27 @@ class CRegTestParams : public CChainParams { { .height = 110, .hash_serialized = AssumeutxoHash{uint256S("0x9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b")}, + // Unit-test chains at this height may have different empty evo + // state encodings, so retain the regtest-only M4 wildcard. .evo_hash = EvoSnapshotHash{uint256{}}, .nChainTx = 110, - .blockhash = uint256{}, + .blockhash = uint256S("0x729bcb1479ff9f4968439f0276bd76bcb2de0f0720b7a16f383321f6a41cb238"), }, { .height = 200, .hash_serialized = AssumeutxoHash{uint256S("0x8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3")}, .evo_hash = EvoSnapshotHash{uint256{}}, .nChainTx = 200, - .blockhash = uint256{}, + .blockhash = uint256S("0x19c1b203b5a960c7f3619e0e805b24c94e684b1aa261f3a79c84d291638a6e1f"), }, { // For use by test/functional/feature_assumeutxo.py. Dash-specific - // hashes are filled in by the test adaptation follow-up. + // pre-DIP3 snapshot has an empty, but canonically serialized, evo section. .height = 299, - .hash_serialized = AssumeutxoHash{uint256{}}, - .evo_hash = EvoSnapshotHash{uint256{}}, + .hash_serialized = AssumeutxoHash{uint256S("0x2618646eb7f9b17a1982e206f94e8feec3efb3b7e7e97ade16b658eef7636519")}, + .evo_hash = EvoSnapshotHash{uint256S("0xf2ccd3fef604df58a0c174489821e16912c9332969a267650cd040e85fb2adde")}, .nChainTx = 300, - .blockhash = uint256{}, + .blockhash = uint256S("0x64ce3ab60754c7974ea472221fa9c7a04f2d193ba480d1ef61b5d12216b14760"), }, }; diff --git a/src/dsnotificationinterface.cpp b/src/dsnotificationinterface.cpp index 43c9345e172a..741dd314975c 100644 --- a/src/dsnotificationinterface.cpp +++ b/src/dsnotificationinterface.cpp @@ -77,8 +77,9 @@ void CDSNotificationInterface::TransactionAddedToMempool(const CTransactionRef& m_dstxman.TransactionAddedToMempool(ptx); } -void CDSNotificationInterface::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) +void CDSNotificationInterface::BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) { + if (role == ChainstateRole::BACKGROUND) return; m_dstxman.BlockConnected(pblock, pindex); } diff --git a/src/dsnotificationinterface.h b/src/dsnotificationinterface.h index bb4a42792cde..2d138a7115db 100644 --- a/src/dsnotificationinterface.h +++ b/src/dsnotificationinterface.h @@ -34,7 +34,7 @@ class CDSNotificationInterface : public CValidationInterface void SynchronousUpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) override; void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) override; void TransactionAddedToMempool(const CTransactionRef& tx, int64_t nAcceptTime, uint64_t mempool_sequence) override; - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) override; + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) override; void BlockDisconnected(const std::shared_ptr& pblock, const CBlockIndex* pindexDisconnected) override; void NotifyMasternodeListChanged(bool undo, const CDeterministicMNList& oldMNList, const CDeterministicMNListDiff& diff) override; void NotifyChainLock(const CBlockIndex* pindex, const std::shared_ptr& clsig) override; diff --git a/src/index/base.cpp b/src/index/base.cpp index dfad899cc180..0de3291ceb39 100644 --- a/src/index/base.cpp +++ b/src/index/base.cpp @@ -89,24 +89,24 @@ bool BaseIndex::Init() // Note: this will latch to true immediately if the user starts up with an empty // datadir and an index enabled. If this is the case, indexation will happen solely // via `BlockConnected` signals until, possibly, the next restart. - m_synced = m_best_block_index.load() == active_chain.Tip(); + m_synced = m_best_block_index.load() == index_chain.Tip(); if (!m_synced) { bool prune_violation = false; if (!m_best_block_index) { // index is not built yet // make sure we have all block data back to the genesis - prune_violation = m_chainstate->m_blockman.GetFirstStoredBlock(*active_chain.Tip()) != active_chain.Genesis(); + prune_violation = m_chainstate->m_blockman.GetFirstStoredBlock(*index_chain.Tip()) != index_chain.Genesis(); } // in case the index has a best block set and is not fully synced // check if we have the required blocks to continue building the index else { const CBlockIndex* block_to_test = m_best_block_index.load(); - if (!active_chain.Contains(block_to_test)) { + if (!index_chain.Contains(block_to_test)) { // if the bestblock is not part of the mainchain, find the fork // and make sure we have all data down to the fork - block_to_test = active_chain.FindFork(block_to_test); + block_to_test = index_chain.FindFork(block_to_test); } - const CBlockIndex* block = active_chain.Tip(); + const CBlockIndex* block = index_chain.Tip(); prune_violation = true; // check backwards from the tip if we have all block data until we reach the indexes bestblock while (block_to_test && block && (block->nStatus & BLOCK_HAVE_DATA)) { diff --git a/src/index/base.h b/src/index/base.h index ad84b8e67ec6..bc79b29fd759 100644 --- a/src/index/base.h +++ b/src/index/base.h @@ -128,9 +128,12 @@ class BaseIndex : public CValidationInterface virtual DB& GetDB() const = 0; +public: /// Get the name of the index for display in logs. const std::string& GetName() const LIFETIMEBOUND { return m_name; } +protected: + /// Trigger a fatal index error and initiate shutdown. static void FatalErrorImpl(const std::string& message); @@ -148,9 +151,6 @@ class BaseIndex : public CValidationInterface /// Destructor interrupts sync thread if running and blocks until it exits. virtual ~BaseIndex(); - /// Get the name of the index for display in logs. - const std::string& GetName() const LIFETIMEBOUND { return m_name; } - /// Blocks the current thread until the index is caught up to the current /// state of the block chain. This only blocks if the index has gotten in /// sync once and only needs to process blocks in the ValidationInterface diff --git a/src/instantsend/net_instantsend.cpp b/src/instantsend/net_instantsend.cpp index 07e86fd780f4..7fc4642c7085 100644 --- a/src/instantsend/net_instantsend.cpp +++ b/src/instantsend/net_instantsend.cpp @@ -73,12 +73,13 @@ bool NetInstantSend::ValidateIncomingISLock(const instantsend::InstantSendLock& std::optional NetInstantSend::ResolveCycleHeight(const uint256& cycle_hash) { - auto cycle_height = GetBlockHeight(m_is_manager, m_chainman.ActiveChainstate(), cycle_hash); + Chainstate& chainstate{m_chainman.ActiveChainstate()}; + auto cycle_height = GetBlockHeight(m_is_manager, chainstate, cycle_hash); if (cycle_height) { return cycle_height; } - const auto block_index = WITH_LOCK(::cs_main, return m_chainman.m_blockman.LookupBlockIndex(cycle_hash)); + const auto block_index = WITH_LOCK(::cs_main, return chainstate.m_blockman.LookupBlockIndex(cycle_hash)); if (block_index == nullptr) { return std::nullopt; } @@ -524,8 +525,9 @@ void NetInstantSend::TransactionRemovedFromMempool(const CTransactionRef& tx, Me m_is_manager.TransactionIsRemoved(tx); } -void NetInstantSend::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) +void NetInstantSend::BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) { + if (role == ChainstateRole::BACKGROUND) return; if (!m_is_manager.IsInstantSendEnabled()) { return; } @@ -595,6 +597,7 @@ void NetInstantSend::ResolveBlockConflicts(const uint256& islockHash, const inst bool hasTxForLock = m_is_manager.HasTxForLock(islockHash); bool activateBestChain = false; + Chainstate& chainstate{m_chainman.ActiveChainstate()}; for (const auto& p : conflicts) { const auto* pindex = p.first; ClearConflicting(p.second); @@ -603,8 +606,8 @@ void NetInstantSend::ResolveBlockConflicts(const uint256& islockHash, const inst BlockValidationState state; // need non-const pointer - auto pindex2 = WITH_LOCK(::cs_main, return m_chainman.ActiveChainstate().m_blockman.LookupBlockIndex(pindex->GetBlockHash())); - if (!m_chainman.ActiveChainstate().InvalidateBlock(state, pindex2)) { + auto pindex2 = WITH_LOCK(::cs_main, return chainstate.m_blockman.LookupBlockIndex(pindex->GetBlockHash())); + if (!chainstate.InvalidateBlock(state, pindex2)) { LogPrintf("NetInstantSend::%s -- InvalidateBlock failed: %s\n", __func__, state.ToString()); // This should not have happened and we are in a state were it's not safe to continue anymore assert(false); @@ -614,13 +617,13 @@ void NetInstantSend::ResolveBlockConflicts(const uint256& islockHash, const inst } else { LogPrintf("NetInstantSend::%s -- resetting block %s\n", __func__, pindex2->GetBlockHash().ToString()); LOCK(::cs_main); - m_chainman.ActiveChainstate().ResetBlockFailureFlags(pindex2); + chainstate.ResetBlockFailureFlags(pindex2); } } if (activateBestChain) { BlockValidationState state; - if (!m_chainman.ActiveChainstate().ActivateBestChain(state)) { + if (!chainstate.ActivateBestChain(state)) { LogPrintf("NetInstantSend::%s -- ActivateBestChain failed: %s\n", __func__, state.ToString()); // This should not have happened and we are in a state were it's not safe to continue anymore assert(false); diff --git a/src/instantsend/net_instantsend.h b/src/instantsend/net_instantsend.h index 9981b7ccd4f3..48e6d6671e72 100644 --- a/src/instantsend/net_instantsend.h +++ b/src/instantsend/net_instantsend.h @@ -14,6 +14,7 @@ #include #include +class Chainstate; class ChainstateManager; namespace Consensus { @@ -44,7 +45,7 @@ class NetInstantSend final : public NetHandler, public CValidationInterface public: NetInstantSend(PeerManagerInternal* peer_manager, llmq::CInstantSendManager& is_manager, instantsend::InstantSendSigner* signer, llmq::CSigningManager& sigman, llmq::CQuorumManager& qman, - const chainlock::Chainlocks& chainlocks, const ChainstateManager& chainman, CTxMemPool& mempool, + const chainlock::Chainlocks& chainlocks, ChainstateManager& chainman, CTxMemPool& mempool, const CMasternodeSync& mn_sync) : NetHandler(peer_manager), m_is_manager{is_manager}, @@ -74,7 +75,7 @@ class NetInstantSend final : public NetHandler, public CValidationInterface void TransactionAddedToMempool(const CTransactionRef&, int64_t, uint64_t mempool_sequence) override; void TransactionRemovedFromMempool(const CTransactionRef& ptx, MemPoolRemovalReason reason, uint64_t mempool_sequence) override; - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) override; + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) override; void BlockDisconnected(const std::shared_ptr& pblock, const CBlockIndex* pindexDisconnected) override; void NotifyChainLock(const CBlockIndex* pindex, const std::shared_ptr& clsig) override; @@ -113,7 +114,7 @@ class NetInstantSend final : public NetHandler, public CValidationInterface llmq::CSigningManager& m_sigman; llmq::CQuorumManager& m_qman; const chainlock::Chainlocks& m_chainlocks; - const ChainstateManager& m_chainman; + ChainstateManager& m_chainman; CTxMemPool& m_mempool; const CMasternodeSync& m_mn_sync; diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index 260d053c23f8..65bcd0df16b1 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -25,6 +25,7 @@ class CRPCCommand; class CScheduler; class CFeeRate; class CBlockIndex; +class CBlockUndo; class Coin; class uint256; enum class MemPoolRemovalReason; @@ -47,6 +48,20 @@ typedef std::shared_ptr CTransactionRef; namespace interfaces { +//! Block data sent with blockConnected and blockDisconnected notifications. +struct BlockInfo { + const uint256& hash; + const uint256* prev_hash{nullptr}; + int height{-1}; + int file_number{-1}; + unsigned data_pos{0}; + const CBlock* data{nullptr}; + const CBlockUndo* undo_data{nullptr}; + unsigned int chain_time_max{0}; + + explicit BlockInfo(const uint256& block_hash) : hash(block_hash) {} +}; + class Wallet; class Handler; diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 96bbee3c09ed..c6f6b81ffefd 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -205,9 +205,11 @@ void BlockManager::FindFilesToPrune( return; } - // Distribute our -prune budget over all chainstates. + // Distribute our -prune budget over all chainstates. On regtest, preserve + // the lower configured target used by pruning tests. const auto target = std::max( - MIN_DISK_SPACE_FOR_BLOCK_FILES, nPruneTarget / chainman.GetAll().size()); + std::min(MIN_DISK_SPACE_FOR_BLOCK_FILES, nPruneTarget), + nPruneTarget / chainman.GetAll().size()); if (target == 0) { return; @@ -231,7 +233,7 @@ void BlockManager::FindFilesToPrune( // To avoid excessive prune events negating the benefit of high dbcache // values, we should not prune too rapidly. // So when pruning in IBD, increase the buffer a bit to avoid a re-prune too soon. - if (chainman.IsInitialBlockDownload()) { + if (chain.IsInitialBlockDownload()) { // Since this is only relevant during IBD, we use a fixed 10% nBuffer += target / 10; } diff --git a/src/node/context.h b/src/node/context.h index ce9a384ecdb6..41760cf77613 100644 --- a/src/node/context.h +++ b/src/node/context.h @@ -40,6 +40,7 @@ class PeerManager; class SpentIndex; class TimestampIndex; struct ActiveContext; +class BaseIndex; struct LLMQContext; namespace chainlock { @@ -114,6 +115,7 @@ struct NodeContext { //! Dash contexts std::unique_ptr ds_notification_interface; std::unique_ptr active_ctx; + std::vector indexes; std::unique_ptr llmq_ctx; std::unique_ptr observer_ctx; //! Dash indexes diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index d15391b6e657..c04cf7e4b09d 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -3182,13 +3182,13 @@ static RPCHelpMan loadtxoutset() "Meanwhile, the original chainstate will complete the initial block download process in " "the background, eventually validating up to the block that the snapshot is based upon.\n\n" - "The result is a usable bitcoind instance that is current with the network tip in a " + "The result is a usable dashd instance that is current with the network tip in a " "matter of minutes rather than hours. UTXO snapshot are typically obtained from " "third-party sources (HTTP, torrent, etc.) which is reasonable since their " "contents are always checked by hash.\n\n" "You can find more information on this process in the `assumeutxo` design " - "document ().", + "document ().", { {"path", RPCArg::Type::STR, @@ -3210,6 +3210,11 @@ static RPCHelpMan loadtxoutset() [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { NodeContext& node = EnsureAnyNodeContext(request.context); + if (node.active_ctx) { + throw JSONRPCError(RPC_MISC_ERROR, + "loadtxoutset is unavailable in masternode mode because active signing contexts cannot be rebound safely"); + } + fs::path path{AbsPathForConfigVal(EnsureArgsman(node), fs::u8path(request.params[0].get_str()))}; FILE* file{fsbridge::fopen(path, "rb")}; @@ -3217,7 +3222,7 @@ static RPCHelpMan loadtxoutset() if (afile.IsNull()) { throw JSONRPCError( RPC_INVALID_PARAMETER, - "Couldn't open file " + path.u8string() + " for reading."); + "Couldn't open file " + fs::PathToString(path) + " for reading."); } SnapshotMetadata metadata; @@ -3255,8 +3260,10 @@ static RPCHelpMan loadtxoutset() RPC_INTERNAL_ERROR, "Timed out waiting for base block header to appear in headers chain"); } - if (!chainman.ActivateSnapshot(afile, metadata, false)) { - throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to load UTXO snapshot " + fs::PathToString(path)); + std::string activation_error; + if (!chainman.ActivateSnapshot(afile, metadata, false, &activation_error)) { + const std::string detail = activation_error.empty() ? "" : ": " + activation_error; + throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to load UTXO snapshot " + fs::PathToString(path) + detail); } CBlockIndex* new_tip{WITH_LOCK(::cs_main, return chainman.ActiveTip())}; diff --git a/src/validation.cpp b/src/validation.cpp index b885e65d99af..058f645af8f0 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3412,7 +3412,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< LOCK(cs_main); // Lock transaction pool for at least as long as it takes for connectTrace to be consumed LOCK(MempoolMutex()); - const bool was_in_ibd = m_chainman.IsInitialBlockDownload(); + const bool was_in_ibd = IsInitialBlockDownload(); CBlockIndex* starting_tip = m_chain.Tip(); bool blocks_connected = false; do { @@ -3460,7 +3460,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< if (!blocks_connected) return true; const CBlockIndex* pindexFork = m_chain.FindFork(starting_tip); - bool still_in_ibd = m_chainman.IsInitialBlockDownload(); + bool still_in_ibd = IsInitialBlockDownload(); if (was_in_ibd && !still_in_ibd) { // Active chainstate has exited IBD. @@ -5635,7 +5635,8 @@ bool DeleteSnapshotChainstateFromDisk() bool ChainstateManager::ActivateSnapshot( AutoFile& coins_file, const SnapshotMetadata& metadata, - bool in_memory) + bool in_memory, + std::string* error) { uint256 base_blockhash = metadata.m_base_blockhash; @@ -5701,8 +5702,9 @@ bool ChainstateManager::ActivateSnapshot( static_cast(current_coinstip_cache_size * SNAPSHOT_CACHE_PERC)); } - auto cleanup_bad_snapshot = [&](const char* reason) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + auto cleanup_bad_snapshot = [&](const std::string& reason) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { LogPrintf("[snapshot] activation failed - %s\n", reason); + if (error) *error = reason; this->ReleaseSnapshotPruneLock(); this->MaybeRebalanceCaches(); @@ -5737,9 +5739,10 @@ bool ChainstateManager::ActivateSnapshot( return false; }; - if (!this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata)) { + std::string population_error; + if (!this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata, &population_error)) { LOCK(::cs_main); - return cleanup_bad_snapshot("population failed"); + return cleanup_bad_snapshot(population_error.empty() ? "population failed" : population_error); } LOCK(::cs_main); // cs_main required for rest of snapshot activation. @@ -5808,7 +5811,8 @@ static void SnapshotUTXOHashBreakpoint() bool ChainstateManager::PopulateAndValidateSnapshot( Chainstate& snapshot_chainstate, AutoFile& coins_file, - const SnapshotMetadata& metadata) + const SnapshotMetadata& metadata, + std::string* error) { // It's okay to release cs_main before we're done using `coins_cache` because we know // that nothing else will be referencing the newly created snapshot_chainstate yet. @@ -5845,7 +5849,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot( // Avoid doing the long population work when the snapshot is already behind // the active chainstate. ActivateSnapshot repeats this check before the swap // in case the active tip advances while the snapshot is being loaded. - if (WITH_LOCK(::cs_main, return !CBlockIndexWorkComparator()(ActiveTip(), snapshot_start_block))) { + if (WITH_LOCK(::cs_main, return !node::CBlockIndexWorkComparator()(ActiveTip(), snapshot_start_block))) { LogPrintf("[snapshot] activation failed - height does not exceed active chainstate\n"); return false; } @@ -5925,12 +5929,14 @@ bool ChainstateManager::PopulateAndValidateSnapshot( } catch (const std::ios_base::failure&) { if (DeploymentActiveAt(*snapshot_start_block, GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) { LogPrintf("[snapshot] missing evo section at DIP3-active base\n"); + if (error) *error = "missing evo section at DIP3-active base"; return false; } } if (evo_marker != 0) { if (evo_marker != evo::EVO_SNAPSHOT_MARKER) { LogPrintf("[snapshot] bad evo section marker (or coins left over) after %d coins\n", coins_count); + if (error) *error = "invalid evo section marker"; return false; } try { @@ -5939,12 +5945,14 @@ bool ChainstateManager::PopulateAndValidateSnapshot( evo_file >> *evo_snapshot; } catch (const std::ios_base::failure&) { LogPrintf("[snapshot] truncated or invalid evo section\n"); + if (error) *error = "truncated or invalid evo section"; return false; } try { uint8_t trailing; coins_file >> trailing; LogPrintf("[snapshot] trailing data after evo section\n"); + if (error) *error = "trailing data after evo section"; return false; } catch (const std::ios_base::failure&) { // EOF immediately after a completely decoded CEvoSnapshot is required. @@ -5953,6 +5961,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot( if (!evo_snapshot && DeploymentActiveAt(*snapshot_start_block, GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) { LogPrintf("[snapshot] UTXO-only snapshot refused at DIP3-active base\n"); + if (error) *error = "UTXO-only snapshot refused at DIP3-active base"; return false; } @@ -5996,6 +6005,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot( LOCK(::cs_main); if (!evo::ValidateEvoSnapshotAgainstChain(*evo_snapshot, *this, snapshot_start_block, evo_error)) { LogPrintf("[snapshot] bad evo snapshot chain data: %s\n", evo_error); + if (error) *error = "invalid evo snapshot chain data: " + evo_error; return false; } } @@ -6005,12 +6015,15 @@ bool ChainstateManager::PopulateAndValidateSnapshot( if (au_data.evo_hash == EvoSnapshotHash{uint256::ZERO} && GetParams().NetworkIDString() != CBaseChainParams::REGTEST) { LogPrintf("[snapshot] null evo snapshot hash is only permitted on regtest\n"); + if (error) *error = "null evo snapshot hash is only permitted on regtest"; return false; } if (au_data.evo_hash != EvoSnapshotHash{uint256::ZERO} && EvoSnapshotHash{actual_evo_hash} != au_data.evo_hash) { LogPrintf("[snapshot] bad evo snapshot hash: expected %s, got %s\n", au_data.evo_hash.ToString(), actual_evo_hash.ToString()); + if (error) *error = strprintf("evo snapshot hash mismatch (expected %s, got %s)", + au_data.evo_hash.ToString(), actual_evo_hash.ToString()); return false; } @@ -6023,10 +6036,12 @@ bool ChainstateManager::PopulateAndValidateSnapshot( CBlock base_block; if (!ReadBlockFromDisk(base_block, snapshot_start_block, GetConsensus()) || base_block.vtx.empty()) { LogPrintf("[snapshot] failed to read available base block for evo CbTx check\n"); + if (error) *error = "failed to read base block for evo CbTx check"; return false; } if (!evo::VerifyEvoSnapshotBaseBlock(*evo_snapshot, base_block, evo_error)) { LogPrintf("[snapshot] evo CbTx cross-check failed: %s\n", evo_error); + if (error) *error = "evo CbTx cross-check failed: " + evo_error; return false; } } else if (DeploymentActiveAt(*snapshot_start_block, GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) { @@ -6786,16 +6801,6 @@ bool ChainstateManager::DeleteSnapshotChainstate() return true; } -ChainstateRole Chainstate::GetRole() const -{ - if (m_chainman.GetAll().size() <= 1) { - return ChainstateRole::NORMAL; - } - return (this != &m_chainman.ActiveChainstate()) ? - ChainstateRole::BACKGROUND : - ChainstateRole::ASSUMEDVALID; -} - const CBlockIndex* ChainstateManager::GetSnapshotBaseBlock() const { return m_active_chainstate ? m_active_chainstate->SnapshotBase() : nullptr; diff --git a/src/validation.h b/src/validation.h index 424529ffda10..133d41a6cb96 100644 --- a/src/validation.h +++ b/src/validation.h @@ -927,7 +927,8 @@ class ChainstateManager [[nodiscard]] bool PopulateAndValidateSnapshot( Chainstate& snapshot_chainstate, AutoFile& coins_file, - const node::SnapshotMetadata& metadata); + const node::SnapshotMetadata& metadata, + std::string* error = nullptr); /** * If a block header hasn't already been seen, call CheckBlockHeader on it, ensure @@ -1070,7 +1071,8 @@ class ChainstateManager //! - Move the new chainstate to `m_snapshot_chainstate` and make it our //! ChainstateActive(). [[nodiscard]] bool ActivateSnapshot( - AutoFile& coins_file, const node::SnapshotMetadata& metadata, bool in_memory); + AutoFile& coins_file, const node::SnapshotMetadata& metadata, bool in_memory, + std::string* error = nullptr); //! Once the background validation chainstate has reached the height which //! is the base of the UTXO snapshot in use, compare its coins to ensure diff --git a/test/lint/lint-circular-dependencies.py b/test/lint/lint-circular-dependencies.py index 1d0a616933fa..bc0a693a42bd 100755 --- a/test/lint/lint-circular-dependencies.py +++ b/test/lint/lint-circular-dependencies.py @@ -33,6 +33,7 @@ "index/base -> node/context -> index/spentindex -> index/base", "index/base -> node/context -> index/timestampindex -> index/base", "banman -> common/bloom -> evo/assetlocktx -> llmq/quorumsman -> llmq/blockprocessor -> net -> banman", + "blockfilter -> evo/specialtx_filter -> evo/providertx -> validation -> kernel/chain -> interfaces/chain.h -> blockfilter", "coinjoin/client -> coinjoin/util -> wallet/wallet -> psbt -> node/transaction -> net_processing -> coinjoin/walletman -> coinjoin/client", "common/bloom -> evo/assetlocktx -> llmq/commitment -> evo/deterministicmns -> evo/simplifiedmns -> merkleblock -> common/bloom", "common/bloom -> evo/assetlocktx -> llmq/quorumsman -> llmq/blockprocessor -> net -> common/bloom", From ec2e7d875dcc2e562372a1494c9cc3686db9e97c Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 13:48:37 -0500 Subject: [PATCH 04/30] test: adapt bitcoin#27596 coverage for Dash --- src/test/validation_chainstate_tests.cpp | 20 ++++- .../validation_chainstatemanager_tests.cpp | 76 ++++++++----------- src/test/validation_tests.cpp | 4 + test/functional/feature_assumeutxo.py | 19 +++-- 4 files changed, 64 insertions(+), 55 deletions(-) diff --git a/src/test/validation_chainstate_tests.cpp b/src/test/validation_chainstate_tests.cpp index 8c73bcc412bd..249e9a9a4b88 100644 --- a/src/test/validation_chainstate_tests.cpp +++ b/src/test/validation_chainstate_tests.cpp @@ -35,11 +35,21 @@ class TipEventCounter final : public CValidationInterface int updated_tip{0}; int mn_list_changed{0}; int chainstate_flushed{0}; + int background_block_connected{0}; + int background_chainstate_flushed{0}; - void BlockConnected(const std::shared_ptr&, const CBlockIndex*) override { ++block_connected; } + void BlockConnected(ChainstateRole role, const std::shared_ptr&, const CBlockIndex*) override + { + ++block_connected; + if (role == ChainstateRole::BACKGROUND) ++background_block_connected; + } void UpdatedBlockTip(const CBlockIndex*, const CBlockIndex*, bool) override { ++updated_tip; } void NotifyMasternodeListChanged(bool, const CDeterministicMNList&, const CDeterministicMNListDiff&) override { ++mn_list_changed; } - void ChainStateFlushed(const CBlockLocator&) override { ++chainstate_flushed; } + void ChainStateFlushed(ChainstateRole role, const CBlockLocator&) override + { + ++chainstate_flushed; + if (role == ChainstateRole::BACKGROUND) ++background_chainstate_flushed; + } }; } // namespace @@ -175,10 +185,12 @@ BOOST_FIXTURE_TEST_CASE(chainstate_update_tip, TestChain100Setup) // validation chain. BOOST_CHECK(block_added); BOOST_CHECK_EQUAL(curr_tip, ::g_best_block); - BOOST_CHECK_EQUAL(event_counter.block_connected, 0); + BOOST_CHECK_EQUAL(event_counter.block_connected, 1); + BOOST_CHECK_EQUAL(event_counter.background_block_connected, 1); BOOST_CHECK_EQUAL(event_counter.updated_tip, 0); BOOST_CHECK_EQUAL(event_counter.mn_list_changed, 0); - BOOST_CHECK_EQUAL(event_counter.chainstate_flushed, 0); + BOOST_CHECK_EQUAL(event_counter.chainstate_flushed, 1); + BOOST_CHECK_EQUAL(event_counter.background_chainstate_flushed, 1); BOOST_CHECK_EQUAL(ui_mn_list_changed, 0); } diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 16d6643405c2..d5d495f97e48 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -53,22 +53,6 @@ void SeedSnapshotMarker(CEvoDB& evodb, const uint256& hash) BOOST_FIXTURE_TEST_SUITE(validation_chainstatemanager_tests, TestingSetup) -static void DashChainstateSetup(ChainstateManager& chainman, - node::NodeContext& node, - bool llmq_dbs_in_memory, - bool llmq_dbs_wipe) -{ - node.llmq_ctx.reset(); - node.llmq_ctx = std::make_unique(*node.dmnman, *node.evodb, chainman, - util::DbWrapperParams{.path = node.args->GetDataDirNet(), .memory = llmq_dbs_in_memory, .wipe = llmq_dbs_wipe}, - llmq::DEFAULT_BLSCHECK_THREADS, llmq::DEFAULT_WORKER_COUNT, llmq::DEFAULT_MAX_RECOVERED_SIGS_AGE); - // Initialize chain_helper - node.chain_helper.reset(); - node.chain_helper = std::make_unique(*node.evodb, *node.dmnman, *Assert(node.mn_sync), *Assert(node.isman), *(node.llmq_ctx->quorum_block_processor), - *(node.llmq_ctx->qsnapman), chainman, chainman.GetConsensus(), *Assert(node.chainlocks), - *(node.llmq_ctx->qman)); -} - static void DashChainstateSetupClose(node::NodeContext& node) { node.chain_helper.reset(); @@ -110,8 +94,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup) BOOST_CHECK(!manager.SnapshotBlockhash().has_value()); - DashChainstateSetupClose(m_node); - // Create a snapshot-based chainstate. // const uint256 snapshot_blockhash = active_tip->GetBlockHash(); @@ -119,12 +101,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup) Chainstate& c2 = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot(snapshot_blockhash)); chainstates.push_back(&c2); - // Only the active chainstate keeps the mempool. - BOOST_CHECK_EQUAL(c2.GetMempool(), &mempool); - BOOST_CHECK(!c1.GetMempool()); - - DashChainstateSetup(manager, m_node, /*llmq_dbs_in_memory=*/true, /*llmq_dbs_wipe=*/false); - BOOST_CHECK_EQUAL(manager.SnapshotBlockhash().value(), snapshot_blockhash); c2.InitCoinsDB( /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); @@ -164,27 +140,18 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup) // Let scheduler events finish running to avoid accessing memory that is going to be unloaded SyncWithValidationInterfaceQueue(); - DashChainstateSetupClose(m_node); - // dmnman holds a reference to m_node.evodb, it mustn't outlive it - m_node.dmnman.reset(); } BOOST_AUTO_TEST_CASE(snapshot_startup_missing_base_header_is_nonfatal) { ChainstateManager& manager = *m_node.chainman; - Chainstate& background = WITH_LOCK(::cs_main, return manager.InitializeChainstate( - m_node.mempool.get(), *m_node.evodb, m_node.chain_helper)); - background.InitCoinsDB(/*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); - WITH_LOCK(::cs_main, background.InitCoinsCache(1 << 23)); - m_node.dmnman = std::make_unique(*m_node.evodb, *Assert(m_node.mn_metaman.get())); - DashChainstateSetup(manager, m_node, /*llmq_dbs_in_memory=*/true, /*llmq_dbs_wipe=*/false); - BOOST_REQUIRE(background.LoadGenesisBlock()); + // The upstream suite now uses TestingSetup, which already initialized the + // IBD chainstate and Dash chainstate consumers. + Chainstate& background = manager.ActiveChainstate(); const uint256 missing_base{GetRandHash()}; SeedSnapshotMarker(*m_node.evodb, missing_base); - Chainstate* snapshot = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot( - m_node.mempool.get(), missing_base)); - BOOST_REQUIRE(snapshot); + Chainstate* snapshot = WITH_LOCK(::cs_main, return &manager.ActivateExistingSnapshot(missing_base)); // Startup detection is allowed to precede receipt/loading of the base // header. Accessors and background candidate setup must fail softly. @@ -195,10 +162,6 @@ BOOST_AUTO_TEST_CASE(snapshot_startup_missing_base_header_is_nonfatal) manager.GetConsensus().hashGenesisBlock)); BOOST_CHECK_EQUAL(background.setBlockIndexCandidates.size(), candidates_before); }); - - DashChainstateSetupClose(m_node); - // dmnman holds a reference to m_node.evodb, it mustn't outlive it - m_node.dmnman.reset(); } BOOST_FIXTURE_TEST_CASE(snapshot_prune_lock_release_survives_disconnect, TestChain100Setup) @@ -510,6 +473,27 @@ struct SnapshotTestSetup : TestChain100Setup { } }; +//! Ensure a height-200 snapshot chainstate can be recovered from disk by block hash. +BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init_height_200, SnapshotTestSetup) +{ + ChainstateManager& chainman = *Assert(m_node.chainman); + mineBlocks(100); + BOOST_REQUIRE_EQUAL(WITH_LOCK(chainman.GetMutex(), return chainman.ActiveHeight()), 200); + BOOST_REQUIRE(CreateAndActivateUTXOSnapshot(this)); + + const auto assumeutxo = Params().AssumeutxoForHeight(200); + BOOST_REQUIRE(assumeutxo); + BOOST_REQUIRE_EQUAL(*chainman.SnapshotBlockhash(), assumeutxo->blockhash); + + ChainstateManager& restarted = this->SimulateNodeRestart(); + this->LoadVerifyActivateChainstate(); + g_txindex = std::make_unique(1 << 20, /*memory=*/true); + BOOST_REQUIRE(g_txindex->Start(restarted.ActiveChainstate())); + IndexWaitSynced(*g_txindex); + + BOOST_CHECK_EQUAL(WITH_LOCK(restarted.GetMutex(), return restarted.ActiveHeight()), 200); +} + //! Test basic snapshot activation. BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, SnapshotTestSetup) { @@ -910,6 +894,12 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_evodb_snapshot_only_flush_restart, Sna Chainstate* background_chainstate = std::get<0>(chainstates); ChainstateManager& chainman = *Assert(m_node.chainman); + // Mine first: background processing now consumes every newly accepted block, + // so disconnecting before this would immediately reconnect the base block. + mineBlocks(1); + const uint256 snapshot_marker = WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip()->GetBlockHash()); + WITH_LOCK(::cs_main, chainman.ActiveChainstate().ForceFlushStateToDisk()); + // Keep this M2 marker-independence test below completion height; #25740 now // completes and cleans up immediately on restart when background is at base. DisconnectedBlockTransactions unused_pool; @@ -926,10 +916,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_evodb_snapshot_only_flush_restart, Sna uint256 normal_marker; BOOST_REQUIRE(m_node.evodb->ReadBestBlock(EvoDbIdentity::NORMAL, normal_marker)); - mineBlocks(1); - const uint256 snapshot_marker = WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip()->GetBlockHash()); - WITH_LOCK(::cs_main, chainman.ActiveChainstate().ForceFlushStateToDisk()); - ChainstateManager& restarted = this->SimulateNodeRestart(/*flush_chainstates=*/false); this->LoadVerifyActivateChainstate(); diff --git a/src/test/validation_tests.cpp b/src/test/validation_tests.cpp index 0784ecc5e99f..34817b9ea56f 100644 --- a/src/test/validation_tests.cpp +++ b/src/test/validation_tests.cpp @@ -35,6 +35,10 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo) BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b"); BOOST_CHECK_EQUAL(out110.nChainTx, 110U); + const auto out110_2 = *params->AssumeutxoForBlockhash(uint256S("0x729bcb1479ff9f4968439f0276bd76bcb2de0f0720b7a16f383321f6a41cb238")); + BOOST_CHECK_EQUAL(out110_2.hash_serialized.ToString(), "9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b"); + BOOST_CHECK_EQUAL(out110_2.nChainTx, 110U); + const auto out210 = *params->AssumeutxoForHeight(200); BOOST_CHECK_EQUAL(out210.hash_serialized.ToString(), "8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3"); BOOST_CHECK_EQUAL(out210.nChainTx, 200U); diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index be1aa1899380..3cd5b05ac5e1 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -4,7 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test for assumeutxo, a means of quickly bootstrapping a node using a serialized version of the UTXO set at a certain height, which corresponds -to a hash that has been compiled into bitcoind. +to a hash that has been compiled into dashd. The assumeutxo value generated and used here is committed to in `CRegTestParams::m_assumeutxo_data` in `src/chainparams.cpp`. @@ -36,6 +36,7 @@ """ from test_framework.test_framework import BitcoinTestFramework +from test_framework.governance import EXPECTED_STDERR_NO_GOV_PRUNE from test_framework.util import assert_equal, wait_until_helper START_HEIGHT = 199 @@ -110,7 +111,8 @@ def no_sync(): assert_equal( dump_output['txoutset_hash'], - 'ef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0') + '2618646eb7f9b17a1982e206f94e8feec3efb3b7e7e97ade16b658eef7636519') + assert_equal(dump_output['evo_hash'], 'f2ccd3fef604df58a0c174489821e16912c9332969a267650cd040e85fb2adde') assert_equal(dump_output['nchaintx'], 300) assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) @@ -139,7 +141,8 @@ def no_sync(): self.log.info("Restarting node to stop at height %d", PAUSE_HEIGHT) self.restart_node(1, extra_args=[ - f"-stopatheight={PAUSE_HEIGHT}", *self.extra_args[1]]) + f"-stopatheight={PAUSE_HEIGHT}", *self.extra_args[1]], + expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) # Finally connect the nodes and let them sync. self.connect_nodes(0, 1) @@ -152,7 +155,7 @@ def no_sync(): assert not self.has_blockfile(n1, "00002"), "too many blockfiles" self.log.info("Restarted node before snapshot validation completed, reloading...") - self.restart_node(1, extra_args=self.extra_args[1]) + self.restart_node(1, extra_args=self.extra_args[1], expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) self.connect_nodes(0, 1) self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})") @@ -174,7 +177,8 @@ def no_sync(): for i in (0, 1): n = self.nodes[i] self.log.info(f"Restarting node {i} to ensure (Check|Load)BlockIndex passes") - self.restart_node(i, extra_args=self.extra_args[i]) + self.restart_node(i, extra_args=self.extra_args[i], + expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE if i == 1 else '') assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT) @@ -232,7 +236,8 @@ def no_sync(): self.log.info("Test -reindex-chainstate of an assumeutxo-synced node") self.restart_node(2, extra_args=[ - '-reindex-chainstate=1', *self.extra_args[2]]) + '-reindex-chainstate=1', *self.extra_args[2], + '-txindex=0', '-blockfilterindex=0', '-coinstatsindex=0']) assert_equal(n2.getblockchaininfo()["blocks"], FINAL_HEIGHT) wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT) @@ -241,6 +246,8 @@ def no_sync(): self.connect_nodes(0, 2) wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT) + self.stop_node(1, expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) + if __name__ == '__main__': AssumeutxoTest().main() From 88869b93cf02b7d724b6d1e842cef72fd4ef8db3 Mon Sep 17 00:00:00 2001 From: fanquake Date: Sat, 7 Oct 2023 11:07:53 +0100 Subject: [PATCH 05/30] Merge bitcoin/bitcoin#28562: AssumeUTXO follow-ups 5d227a68627614efa8618d360efee22a47afa88b rpc: Use Ensure(Any)Chainman in assumeutxo related RPCs (Fabian Jahr) 710e5db61bf7b303fa425f8dcbdce536281fa7f3 doc: Drop references to assumevalid in assumeutxo docs (Fabian Jahr) 1ff1c34656d49d60a93066a886dc1bfad9baccf4 test: Rename wait_until_helper to wait_until_helper_internal (Fabian Jahr) a482f86779a6182d87004b463c0eaf21038181c3 chain: Rename HaveTxsDownloaded to HaveNumChainTxs (Fabian Jahr) 82e48d20f1243fb7733e872a29661b151ab5d523 blockstorage: Let FlushChainstateBlockFile return true in case of missing cursor (Fabian Jahr) 73700fb554d6abad705d8f48aed4840fedb36c79 validation, test: Improve and document nChainTx check for testability (Fabian Jahr) 2c9354facb27a6c394bb0c64f85fc4e3a33f4aed doc: Add snapshot chainstate removal warning to reindexing documentation (Fabian Jahr) 4e915e926bccbc9bdd61933ce44e87f2b4173b30 test: Improvements of feature_assumeutxo (Fabian Jahr) a47fbe7d49e8921214ac159c558ff4ca19f98dce doc: Add and edit some comments around assumeutxo (Fabian Jahr) 0a39b8cbd88e9a496823b36feed77d137ccd894c validation: remove unused mempool param in DetectSnapshotChainstate (Fabian Jahr) Pull request description: Addressing what I consider to be non- or not-too-controversial comments from #27596. Let me know if I missed anything among the many comments that can be easily included here. ACKs for top commit: ryanofsky: Code review ACK 5d227a68627614efa8618d360efee22a47afa88b. Just suggested doc change and new EnsureChainman RPC cleanup commit since last review. Tree-SHA512: 6f7c762100e18f82946b881676db23e67da7dc3a8bf04e4999a183e90b4f150a0b1202bcb95920ba937a358867bbf2eca300bd84b9b1776c7c490410e707c267 --- doc/design/assumeutxo.md | 2 +- doc/release-notes-27596.md | 2 +- src/chain.h | 4 +-- src/chainparams.cpp | 4 +-- src/init.cpp | 4 +-- src/net_processing.cpp | 6 ++-- src/node/blockstorage.cpp | 8 +++-- src/rpc/blockchain.cpp | 5 ++- src/test/fuzz/chain.cpp | 2 +- src/test/validation_tests.cpp | 6 ++-- src/validation.cpp | 35 ++++++++++--------- src/validation.h | 18 +++++----- test/functional/feature_assumeutxo.py | 21 +++++------ test/functional/test_framework/p2p.py | 6 ++-- .../test_framework/test_framework.py | 4 +-- test/functional/test_framework/test_node.py | 12 +++---- test/functional/test_framework/util.py | 6 +++- 17 files changed, 74 insertions(+), 71 deletions(-) diff --git a/doc/design/assumeutxo.md b/doc/design/assumeutxo.md index b6f52845cbda..573339395d9b 100644 --- a/doc/design/assumeutxo.md +++ b/doc/design/assumeutxo.md @@ -1,7 +1,7 @@ # assumeutxo Assumeutxo is a feature that allows fast bootstrapping of a validating dashd -instance with a very similar security model to assumevalid. +instance. The RPC commands `dumptxoutset` and `loadtxoutset` are used to respectively generate and load UTXO snapshots. The utility script diff --git a/doc/release-notes-27596.md b/doc/release-notes-27596.md index 2789af210dae..7c74d36d47f2 100644 --- a/doc/release-notes-27596.md +++ b/doc/release-notes-27596.md @@ -12,7 +12,7 @@ RPC `loadtxoutset` has been added, which allows loading a UTXO snapshot of the format generated by `dumptxoutset`. Once this snapshot is loaded, its contents will be deserialized into a second chainstate data structure, which is then used to sync to -the network's tip under a security model very much like `assumevalid`. +the network's tip. Meanwhile, the original chainstate will complete the initial block download process in the background, eventually validating up to the block that the snapshot is based upon. diff --git a/src/chain.h b/src/chain.h index d8926c743728..d25016360f38 100644 --- a/src/chain.h +++ b/src/chain.h @@ -265,10 +265,8 @@ class CBlockIndex * Note that this will be true for the snapshot base block, if one is loaded (and * all subsequent assumed-valid blocks) since its nChainTx value will have been set * manually based on the related AssumeutxoData entry. - * - * TODO: potentially change the name of this based on the fact above. */ - bool HaveTxsDownloaded() const { return nChainTx != 0; } + bool HaveNumChainTxs() const { return nChainTx != 0; } NodeSeconds Time() const { diff --git a/src/chainparams.cpp b/src/chainparams.cpp index e36530701d17..a8ee1e37dab5 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -886,14 +886,14 @@ class CRegTestParams : public CChainParams { // Unit-test chains at this height may have different empty evo // state encodings, so retain the regtest-only M4 wildcard. .evo_hash = EvoSnapshotHash{uint256{}}, - .nChainTx = 110, + .nChainTx = 111, .blockhash = uint256S("0x729bcb1479ff9f4968439f0276bd76bcb2de0f0720b7a16f383321f6a41cb238"), }, { .height = 200, .hash_serialized = AssumeutxoHash{uint256S("0x8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3")}, .evo_hash = EvoSnapshotHash{uint256{}}, - .nChainTx = 200, + .nChainTx = 201, .blockhash = uint256S("0x19c1b203b5a960c7f3619e0e805b24c94e684b1aa261f3a79c84d291638a6e1f"), }, { diff --git a/src/init.cpp b/src/init.cpp index b17b6f1c09c7..6aa4616c6db9 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -618,8 +618,8 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-version", "Print version and exit", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-addressindex", strprintf("Maintain a full address index, used to query for the balance, txids and unspent outputs for addresses (default: %u)", DEFAULT_ADDRESSINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); - argsman.AddArg("-reindex", "Rebuild chain state and block index from the blk*.dat files on disk. This will also rebuild active optional indexes.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); - argsman.AddArg("-reindex-chainstate", "Rebuild chain state from the currently indexed blocks. When in pruning mode or if blocks on disk might be corrupted, use full -reindex instead. Deactivate all optional indexes before running this.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); + argsman.AddArg("-reindex", "If enabled, wipe chain state and block index, and rebuild them from blk*.dat files on disk. Also wipe and rebuild other optional indexes that are active. If an assumeutxo snapshot was loaded, its chainstate will be wiped as well. The snapshot can then be reloaded via RPC.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); + argsman.AddArg("-reindex-chainstate", "If enabled, wipe chain state, and rebuild it from the currently indexed blocks. If an assumeutxo snapshot was loaded, its chainstate will be wiped as well. The snapshot can then be reloaded via RPC. When in pruning mode or if blocks on disk might be corrupted, use full -reindex instead. Deactivate all optional indexes before running this.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); argsman.AddArg("-spentindex", strprintf("Maintain a full spent index, used to query the spending txid and input index for an outpoint (default: %u)", DEFAULT_SPENTINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); argsman.AddArg("-timestampindex", strprintf("Maintain a timestamp index for block hashes, used to query blocks hashes by a range of timestamps (default: %u)", DEFAULT_TIMESTAMPINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); argsman.AddArg("-txindex", strprintf("Maintain a full transaction index, used by the getrawtransaction rpc call (default: %u)", DEFAULT_TXINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 8bf2addfc162..d1f7fdf78d4c 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1545,7 +1545,7 @@ void PeerManagerImpl::FindNextBlocks(std::vector& vBlocks, c return; } if (pindex->nStatus & BLOCK_HAVE_DATA || (activeChain && activeChain->Contains(pindex))) { - if (activeChain && pindex->HaveTxsDownloaded()) + if (activeChain && pindex->HaveNumChainTxs()) state->pindexLastCommonBlock = pindex; } else if (!IsBlockRequested(pindex->GetBlockHash())) { // The block is not already downloaded, and not yet in flight. @@ -2167,6 +2167,8 @@ void PeerManagerImpl::BlockConnected( } } + // The following task can be skipped since we don't maintain a mempool for + // the ibd/background chainstate. if (role == ChainstateRole::BACKGROUND) { return; } @@ -2724,7 +2726,7 @@ void PeerManagerImpl::ProcessGetBlockData(CNode& pfrom, Peer& peer, const CInv& LOCK(cs_main); const CBlockIndex* pindex = m_chainman.m_blockman.LookupBlockIndex(inv.hash); if (pindex) { - if (pindex->HaveTxsDownloaded() && !pindex->IsValid(BLOCK_VALID_SCRIPTS) && + if (pindex->HaveNumChainTxs() && !pindex->IsValid(BLOCK_VALID_SCRIPTS) && pindex->IsValid(BLOCK_VALID_TREE)) { // If we have the block and all of its parents, but have not yet validated it, // we might be in the middle of connecting it (ie in the unlock of cs_main diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index c6f6b81ffefd..ba857b3aa848 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -685,12 +685,14 @@ bool BlockManager::FlushChainstateBlockFile(int tip_height) { LOCK(cs_LastBlockFile); auto& cursor = m_blockfile_cursors[BlockfileTypeForHeight(tip_height)]; + // If the cursor does not exist, it means an assumeutxo snapshot is loaded, + // but no blocks past the snapshot height have been written yet, so there + // is no data associated with the chainstate, and it is safe not to flush. if (cursor) { - // The cursor may not exist after a snapshot has been loaded but before any - // blocks have been downloaded. return FlushBlockFile(cursor->file_num, /*fFinalize=*/false, /*finalize_undo=*/false); } - return false; + // No need to log warnings in this case. + return true; } uint64_t BlockManager::CalculateCurrentUsage() diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index c04cf7e4b09d..6fa2ed8955d9 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1852,7 +1852,7 @@ static RPCHelpMan getchaintips() } else if (block->nStatus & BLOCK_CONFLICT_CHAINLOCK) { // This block or one of its ancestors is conflicting with ChainLocks. status = "conflicting"; - } else if (!block->HaveTxsDownloaded()) { + } else if (!block->HaveNumChainTxs()) { // This block cannot be connected because full block data for it or one of its parents is missing. status = "headers-only"; } else if (block->IsValid(BLOCK_VALID_SCRIPTS)) { @@ -3309,8 +3309,7 @@ return RPCHelpMan{ LOCK(cs_main); UniValue obj(UniValue::VOBJ); - NodeContext& node = EnsureAnyNodeContext(request.context); - ChainstateManager& chainman = *node.chainman; + ChainstateManager& chainman = EnsureAnyChainman(request.context); auto make_chain_data = [&](const Chainstate& cs) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); diff --git a/src/test/fuzz/chain.cpp b/src/test/fuzz/chain.cpp index 01994e41ec7f..ff25a7b68a39 100644 --- a/src/test/fuzz/chain.cpp +++ b/src/test/fuzz/chain.cpp @@ -29,7 +29,7 @@ FUZZ_TARGET(chain) (void)disk_block_index->GetBlockTimeMax(); (void)disk_block_index->GetMedianTimePast(); (void)disk_block_index->GetUndoPos(); - (void)disk_block_index->HaveTxsDownloaded(); + (void)disk_block_index->HaveNumChainTxs(); (void)disk_block_index->IsValid(); } diff --git a/src/test/validation_tests.cpp b/src/test/validation_tests.cpp index 34817b9ea56f..ab8f9bcafb92 100644 --- a/src/test/validation_tests.cpp +++ b/src/test/validation_tests.cpp @@ -33,15 +33,15 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo) const auto out110 = *params->AssumeutxoForHeight(110); BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b"); - BOOST_CHECK_EQUAL(out110.nChainTx, 110U); + BOOST_CHECK_EQUAL(out110.nChainTx, 111U); const auto out110_2 = *params->AssumeutxoForBlockhash(uint256S("0x729bcb1479ff9f4968439f0276bd76bcb2de0f0720b7a16f383321f6a41cb238")); BOOST_CHECK_EQUAL(out110_2.hash_serialized.ToString(), "9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b"); - BOOST_CHECK_EQUAL(out110_2.nChainTx, 110U); + BOOST_CHECK_EQUAL(out110_2.nChainTx, 111U); const auto out210 = *params->AssumeutxoForHeight(200); BOOST_CHECK_EQUAL(out210.hash_serialized.ToString(), "8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3"); - BOOST_CHECK_EQUAL(out210.nChainTx, 200U); + BOOST_CHECK_EQUAL(out210.nChainTx, 201U); } //! Test the Dash (non-witness) IsBlockMutated() predicate directly. diff --git a/src/validation.cpp b/src/validation.cpp index 058f645af8f0..32eb0932aa9c 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3183,7 +3183,7 @@ CBlockIndex* Chainstate::FindMostWorkChain() CBlockIndex *pindexTest = pindexNew; bool fInvalidAncestor = false; while (pindexTest && !m_chain.Contains(pindexTest)) { - assert(pindexTest->HaveTxsDownloaded() || pindexTest->nHeight == 0); + assert(pindexTest->HaveNumChainTxs() || pindexTest->nHeight == 0); // Pruned nodes may have entries in setBlockIndexCandidates for // which block files have been deleted. Remove those as candidates @@ -3546,7 +3546,7 @@ bool Chainstate::PreciousBlock(BlockValidationState& state, CBlockIndex* pindex) // call preciousblock 2**31-1 times on the same set of tips... m_chainman.nBlockReverseSequenceId--; } - if (pindex->IsValid(BLOCK_VALID_TRANSACTIONS) && !(pindex->nStatus & BLOCK_CONFLICT_CHAINLOCK) && pindex->HaveTxsDownloaded()) { + if (pindex->IsValid(BLOCK_VALID_TRANSACTIONS) && !(pindex->nStatus & BLOCK_CONFLICT_CHAINLOCK) && pindex->HaveNumChainTxs()) { setBlockIndexCandidates.insert(pindex); PruneBlockIndexCandidates(); } @@ -3595,7 +3595,7 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde !CBlockIndexWorkComparator()(candidate, pindex->pprev) && candidate->IsValid(BLOCK_VALID_TRANSACTIONS) && !(candidate->nStatus & BLOCK_CONFLICT_CHAINLOCK) && - candidate->HaveTxsDownloaded()) { + candidate->HaveNumChainTxs()) { candidate_blocks_by_work.insert(std::make_pair(candidate->nChainWork, candidate)); } } @@ -3695,7 +3695,7 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde // Loop back over all block index entries and add any missing entries // to setBlockIndexCandidates. for (auto& [_, block_index] : m_blockman.m_block_index) { - if (block_index.IsValid(BLOCK_VALID_TRANSACTIONS) && !(block_index.nStatus & BLOCK_CONFLICT_CHAINLOCK) && block_index.HaveTxsDownloaded() && !setBlockIndexCandidates.value_comp()(&block_index, m_chain.Tip())) { + if (block_index.IsValid(BLOCK_VALID_TRANSACTIONS) && !(block_index.nStatus & BLOCK_CONFLICT_CHAINLOCK) && block_index.HaveNumChainTxs() && !setBlockIndexCandidates.value_comp()(&block_index, m_chain.Tip())) { setBlockIndexCandidates.insert(&block_index); } } @@ -3799,7 +3799,7 @@ bool Chainstate::MarkConflictingBlock(BlockValidationState& state, CBlockIndex * // add it again. BlockMap::iterator it = m_blockman.m_block_index.begin(); while (it != m_blockman.m_block_index.end()) { - if (it->second.IsValid(BLOCK_VALID_TRANSACTIONS) && !(it->second.nStatus & BLOCK_CONFLICT_CHAINLOCK) && it->second.HaveTxsDownloaded() && !setBlockIndexCandidates.value_comp()(&it->second, m_chain.Tip())) { + if (it->second.IsValid(BLOCK_VALID_TRANSACTIONS) && !(it->second.nStatus & BLOCK_CONFLICT_CHAINLOCK) && it->second.HaveNumChainTxs() && !setBlockIndexCandidates.value_comp()(&it->second, m_chain.Tip())) { setBlockIndexCandidates.insert(&it->second); } it++; @@ -3892,7 +3892,7 @@ void Chainstate::ResetBlockFailureFlags(CBlockIndex *pindex, bool ignore_chainlo // candidate admission must be recomputed for all of them as well. const auto chainstates{m_chainman.GetAll()}; for (CBlockIndex* reconsidered : reconsidered_blocks) { - if (!reconsidered->IsValid(BLOCK_VALID_TRANSACTIONS) || !reconsidered->HaveTxsDownloaded()) continue; + if (!reconsidered->IsValid(BLOCK_VALID_TRANSACTIONS) || !reconsidered->HaveNumChainTxs()) continue; for (Chainstate* chainstate : chainstates) { chainstate->TryAddBlockIndexCandidate(reconsidered); } @@ -3946,7 +3946,7 @@ void ChainstateManager::ReceivedBlockTransactions(const CBlock& block, CBlockInd pindexNew->RaiseValidity(BLOCK_VALID_TRANSACTIONS); m_blockman.m_dirty_blockindex.insert(pindexNew); - if (pindexNew->pprev == nullptr || pindexNew->pprev->HaveTxsDownloaded()) { + if (pindexNew->pprev == nullptr || pindexNew->pprev->HaveNumChainTxs()) { // If pindexNew is the genesis block or all parents are BLOCK_VALID_TRANSACTIONS. std::deque queue; queue.push_back(pindexNew); @@ -4970,7 +4970,7 @@ bool ChainstateManager::LoadBlockIndex() // here. if (pindex == GetSnapshotBaseBlock() || (pindex->IsValid(BLOCK_VALID_TRANSACTIONS) && - (pindex->HaveTxsDownloaded() || pindex->pprev == nullptr))) { + (pindex->HaveNumChainTxs() || pindex->pprev == nullptr))) { for (Chainstate* chainstate : GetAll()) { chainstate->TryAddBlockIndexCandidate(pindex); @@ -5268,10 +5268,13 @@ void ChainstateManager::CheckBlockIndex() CBlockIndex* pindexFirstAssumeValid = nullptr; // Oldest ancestor of pindex which has BLOCK_ASSUMED_VALID while (pindex != nullptr) { nNodes++; - if (pindex->pprev && pindex->nTx > 0) { - // nChainTx should increase monotonically - assert(pindex->pprev->nChainTx <= pindex->nChainTx); - } + // Make sure nChainTx sum is correctly computed. + unsigned int prev_chain_tx = pindex->pprev ? pindex->pprev->nChainTx : 0; + assert((pindex->nChainTx == pindex->nTx + prev_chain_tx) + // For testing, allow transaction counts to be completely unset. + || (pindex->nChainTx == 0 && pindex->nTx == 0) + // For testing, allow this nChainTx to be unset if previous is also unset. + || (pindex->nChainTx == 0 && prev_chain_tx == 0 && pindex->pprev)); if (pindexFirstAssumeValid == nullptr && pindex->nStatus & BLOCK_ASSUMED_VALID) pindexFirstAssumeValid = pindex; if (pindexFirstInvalid == nullptr && pindex->nStatus & BLOCK_FAILED_VALID) pindexFirstInvalid = pindex; if (pindexFirstConflicing == nullptr && pindex->nStatus & BLOCK_CONFLICT_CHAINLOCK) pindexFirstConflicing = pindex; @@ -5311,7 +5314,7 @@ void ChainstateManager::CheckBlockIndex() } } } - if (!pindex->HaveTxsDownloaded()) assert(pindex->nSequenceId <= 0); // nSequenceId can't be set positive for blocks that aren't linked (negative is used for preciousblock) + if (!pindex->HaveNumChainTxs()) assert(pindex->nSequenceId <= 0); // nSequenceId can't be set positive for blocks that aren't linked (negative is used for preciousblock) // VALID_TRANSACTIONS is equivalent to nTx > 0 for all nodes (whether or not pruning has occurred). // HAVE_DATA is only equivalent to nTx > 0 (or VALID_TRANSACTIONS) if no pruning has occurred. // Unless these indexes are assumed valid and pending block download on a @@ -5341,9 +5344,9 @@ void ChainstateManager::CheckBlockIndex() // actually seen a block's transactions. assert(((pindex->nStatus & BLOCK_VALID_MASK) >= BLOCK_VALID_TRANSACTIONS) == (pindex->nTx > 0)); // This is pruning-independent. } - // All parents having had data (at some point) is equivalent to all parents being VALID_TRANSACTIONS, which is equivalent to HaveTxsDownloaded(). - assert((pindexFirstNeverProcessed == nullptr) == pindex->HaveTxsDownloaded()); - assert((pindexFirstNotTransactionsValid == nullptr) == pindex->HaveTxsDownloaded()); + // All parents having had data (at some point) is equivalent to all parents being VALID_TRANSACTIONS, which is equivalent to HaveNumChainTxs(). + assert((pindexFirstNeverProcessed == nullptr) == pindex->HaveNumChainTxs()); + assert((pindexFirstNotTransactionsValid == nullptr) == pindex->HaveNumChainTxs()); assert(pindex->nHeight == nHeight); // nHeight must be consistent. assert(pindex->pprev == nullptr || pindex->nChainWork >= pindex->pprev->nChainWork); // For every block except the genesis block, the chainwork must be larger than the parent's. assert(nHeight < 2 || (pindex->pskip && (pindex->pskip->nHeight < nHeight))); // The pskip pointer must point back for all but the first 2 blocks. diff --git a/src/validation.h b/src/validation.h index 133d41a6cb96..3012ff7e3dc4 100644 --- a/src/validation.h +++ b/src/validation.h @@ -897,9 +897,10 @@ class ChainstateManager //! Once this pointer is set to a corresponding chainstate, it will not //! be reset until init.cpp:Shutdown(). //! - //! This is especially important when, e.g., calling ActivateBestChain() - //! on all chainstates because we are not able to hold ::cs_main going into - //! that call. + //! It is important for the pointer to not be deleted until shutdown, + //! because cs_main is not always held when the pointer is accessed, for + //! example when calling ActivateBestChain, so there's no way you could + //! prevent code from using the pointer while deleting it. std::unique_ptr m_ibd_chainstate GUARDED_BY(::cs_main); //! A chainstate initialized on the basis of a UTXO snapshot. If this is @@ -908,17 +909,14 @@ class ChainstateManager //! Once this pointer is set to a corresponding chainstate, it will not //! be reset until init.cpp:Shutdown(). //! - //! This is especially important when, e.g., calling ActivateBestChain() - //! on all chainstates because we are not able to hold ::cs_main going into - //! that call. + //! It is important for the pointer to not be deleted until shutdown, + //! because cs_main is not always held when the pointer is accessed, for + //! example when calling ActivateBestChain, so there's no way you could + //! prevent code from using the pointer while deleting it. std::unique_ptr m_snapshot_chainstate GUARDED_BY(::cs_main); //! Points to either the ibd or snapshot chainstate; indicates our //! most-work chain. - //! - //! This is especially important when, e.g., calling ActivateBestChain() - //! on all chainstates because we are not able to hold ::cs_main going into - //! that call. Chainstate* m_active_chainstate GUARDED_BY(::cs_main) {nullptr}; CBlockIndex* m_best_invalid GUARDED_BY(::cs_main){nullptr}; diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 3cd5b05ac5e1..a8d5e56b7144 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -37,7 +37,7 @@ """ from test_framework.test_framework import BitcoinTestFramework from test_framework.governance import EXPECTED_STDERR_NO_GOV_PRUNE -from test_framework.util import assert_equal, wait_until_helper +from test_framework.util import assert_equal START_HEIGHT = 199 SNAPSHOT_BASE_HEIGHT = 299 @@ -81,16 +81,13 @@ def run_test(self): self.sync_blocks() - def no_sync(): - pass - # Generate a series of blocks that `n0` will have in the snapshot, # but that n1 doesn't yet see. In order for the snapshot to activate, # though, we have to ferry over the new headers to n1 so that it # isn't waiting forever to see the header of the snapshot's base block # while disconnected from n0. for i in range(100): - self.generate(n0, nblocks=1, sync_fun=no_sync) + self.generate(n0, nblocks=1, sync_fun=self.no_op) newblock = n0.getblock(n0.getbestblockhash(), 0) # make n1 aware of the new header, but don't give it the block. @@ -118,7 +115,7 @@ def no_sync(): # Mine more blocks on top of the snapshot that n1 hasn't yet seen. This # will allow us to test n1's sync-to-tip on top of a snapshot. - self.generate(n0, nblocks=100, sync_fun=no_sync) + self.generate(n0, nblocks=100, sync_fun=self.no_op) assert_equal(n0.getblockcount(), FINAL_HEIGHT) assert_equal(n1.getblockcount(), START_HEIGHT) @@ -159,12 +156,12 @@ def no_sync(): self.connect_nodes(0, 1) self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})") - wait_until_helper(lambda: n1.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) + self.wait_until(lambda: n1.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) self.sync_blocks(nodes=(n0, n1)) self.log.info("Ensuring background validation completes") # N.B.: the `snapshot` key disappears once the background validation is complete. - wait_until_helper(lambda: not n1.getchainstates().get('snapshot')) + self.wait_until(lambda: not n1.getchainstates().get('snapshot')) # Ensure indexes have synced. completed_idx_state = { @@ -207,11 +204,11 @@ def no_sync(): assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash']) self.connect_nodes(0, 2) - wait_until_helper(lambda: n2.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) + self.wait_until(lambda: n2.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) self.sync_blocks() self.log.info("Ensuring background validation completes") - wait_until_helper(lambda: not n2.getchainstates().get('snapshot')) + self.wait_until(lambda: not n2.getchainstates().get('snapshot')) completed_idx_state = { 'basic block filter index': COMPLETE_IDX, @@ -239,12 +236,12 @@ def no_sync(): '-reindex-chainstate=1', *self.extra_args[2], '-txindex=0', '-blockfilterindex=0', '-coinstatsindex=0']) assert_equal(n2.getblockchaininfo()["blocks"], FINAL_HEIGHT) - wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT) + self.wait_until(lambda: n2.getblockcount() == FINAL_HEIGHT) self.log.info("Test -reindex of an assumeutxo-synced node") self.restart_node(2, extra_args=['-reindex=1', *self.extra_args[2]]) self.connect_nodes(0, 2) - wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT) + self.wait_until(lambda: n2.getblockcount() == FINAL_HEIGHT) self.stop_node(1, expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py index a76d956a0301..6fbd8f95cf8d 100755 --- a/test/functional/test_framework/p2p.py +++ b/test/functional/test_framework/p2p.py @@ -89,7 +89,7 @@ from test_framework.util import ( MAX_NODES, p2p_port, - wait_until_helper, + wait_until_helper_internal, ) from test_framework.v2_p2p import ( EncryptedP2PState, @@ -652,7 +652,7 @@ def test_function(): assert self.is_connected return test_function_in() - wait_until_helper(test_function, timeout=timeout, lock=p2p_lock, timeout_factor=self.timeout_factor) + wait_until_helper_internal(test_function, timeout=timeout, lock=p2p_lock, timeout_factor=self.timeout_factor) def wait_for_connect(self, *, timeout=60): test_function = lambda: self.is_connected @@ -799,7 +799,7 @@ def run(self): def close(self, *, timeout=10): """Close the connections and network event loop.""" self.network_event_loop.call_soon_threadsafe(self.network_event_loop.stop) - wait_until_helper(lambda: not self.network_event_loop.is_running(), timeout=timeout) + wait_until_helper_internal(lambda: not self.network_event_loop.is_running(), timeout=timeout) self.network_event_loop.close() self.join(timeout) # Safe to remove event loop. diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 3d342402ec8d..93ff12af4f42 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -54,7 +54,7 @@ set_node_times, satoshi_round, softfork_active, - wait_until_helper, + wait_until_helper_internal, get_chain_folder, write_config, ) @@ -905,7 +905,7 @@ def _initialize_mocktime(self, is_genesis): node.mocktime = self.mocktime def wait_until(self, test_function, timeout=60, lock=None, sleep=0.05, do_assert=True): - return wait_until_helper(test_function, timeout=timeout, lock=lock, timeout_factor=self.options.timeout_factor, sleep=sleep, do_assert=do_assert) + return wait_until_helper_internal(test_function, timeout=timeout, lock=lock, timeout_factor=self.options.timeout_factor, sleep=sleep, do_assert=do_assert) # Private helper methods. These should not be accessed by the subclass test scripts. diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 70ab23f35c11..69753c15b084 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -37,7 +37,7 @@ get_auth_cookie, get_rpc_proxy, rpc_url, - wait_until_helper, + wait_until_helper_internal, p2p_port, get_chain_folder, ) @@ -285,7 +285,7 @@ def wait_for_rpc_connection(self): if self.version_is_at_least(180000): # getmempoolinfo.loaded is available since commit # 71e38b9ebcb78b3a264a4c25c7c4e373317f2a40 (version 0.18.0) - wait_until_helper(lambda: rpc.getmempoolinfo()['loaded']) + wait_until_helper_internal(lambda: rpc.getmempoolinfo()['loaded']) # Wait for the node to finish reindex, block import, and # loading the mempool. Usually importing happens fast or # even "immediate" when the node is started. However, there @@ -434,7 +434,7 @@ def is_node_stopped(self, expected_ret_code=None): def wait_until_stopped(self, timeout=BITCOIND_PROC_WAIT_TIMEOUT, expect_error=False): expected_ret_code = 1 if expect_error else None # Whether node shutdown return EXIT_FAILURE or EXIT_SUCCESS - wait_until_helper(lambda: self.is_node_stopped(expected_ret_code=expected_ret_code), timeout=timeout, timeout_factor=self.timeout_factor) + wait_until_helper_internal(lambda: self.is_node_stopped(expected_ret_code=expected_ret_code), timeout=timeout, timeout_factor=self.timeout_factor) def replace_in_config(self, replacements): """ @@ -546,7 +546,7 @@ def get_highest_peer_id(): initial_peer_id = get_highest_peer_id() yield - wait_until_helper(lambda: get_highest_peer_id() > initial_peer_id, + wait_until_helper_internal(lambda: get_highest_peer_id() > initial_peer_id, timeout=timeout, timeout_factor=self.timeout_factor) @contextlib.contextmanager @@ -812,11 +812,11 @@ def check_peers(): if p['subver'] == p2p.strSubVer: return False return True - wait_until_helper(check_peers, timeout=5) + wait_until_helper_internal(check_peers, timeout=5) del self.p2ps[:] - wait_until_helper(lambda: self.num_test_p2p_connections() == 0, timeout_factor=self.timeout_factor) + wait_until_helper_internal(lambda: self.num_test_p2p_connections() == 0, timeout_factor=self.timeout_factor) class TestNodeCLIAttr: diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index ff5ef5969d42..375b7b134412 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -258,7 +258,7 @@ def satoshi_round(amount): return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) -def wait_until_helper(predicate, *, attempts=float('inf'), timeout=float('inf'), sleep=0.05, timeout_factor=1.0, lock=None, do_assert=True, allow_exception=False): +def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=float('inf'), sleep=0.05, timeout_factor=1.0, lock=None, do_assert=True, allow_exception=False): """Sleep until the predicate resolves to be True. Warning: Note that this method is not recommended to be used in tests as it is @@ -301,6 +301,10 @@ def wait_until_helper(predicate, *, attempts=float('inf'), timeout=float('inf'), return False +# Dash tests outside the framework internals still use this public helper. +wait_until_helper = wait_until_helper_internal + + def sha256sum_file(filename): h = hashlib.sha256() with open(filename, 'rb') as f: From c9bef737a18c4a7205028555ccbc3e67c6d02080 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 4 Oct 2023 13:14:57 -0400 Subject: [PATCH 06/30] Merge bitcoin/bitcoin#28589: test: assumeutxo func test race fixes 7e4003226030a04a19c718a4b1b83b4ca40ca33f tests: assumeutxo: accept final height from either chainstate (James O'Beirne) 5bd2010f024b5bcccf1d57bae6fc36c53f5facc5 test: assumeutxo: avoid race in functional test (James O'Beirne) 7005a01c19001ab5821731597656f8bc5e8c11e3 test: add wait_for_connect to BitcoinTestFramework.connect_nodes (James O'Beirne) Pull request description: Fixes https://github.com/bitcoin/bitcoin/issues/28585. Fixes a few races within the assumeutxo tests: - In general, `-stopatheight` can't be used with `connect_nodes` safely because the latter performs blocking assertions that are racy with the stopatheight triggering. - Now that the snapshot chainstate is listed as `normal` after background validation, accept the final height from either chainstate. ACKs for top commit: MarcoFalke: lgtm ACK 7e4003226030a04a19c718a4b1b83b4ca40ca33f fjahr: Code review ACK 7e4003226030a04a19c718a4b1b83b4ca40ca33f achow101: ACK 7e4003226030a04a19c718a4b1b83b4ca40ca33f ryanofsky: Code review ACK 7e4003226030a04a19c718a4b1b83b4ca40ca33f Tree-SHA512: 8cbd2a0ca8643f94baa0ae3561dcf68c3519d5ba851c6049e1768f28cae6434f47ffc28d404bf38ed11030ce3f00aae0a8be3f6d563e6ae6680d83c928a173d8 --- test/functional/feature_assumeutxo.py | 15 ++++++++++++--- .../test_framework/test_framework.py | 18 ++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index a8d5e56b7144..5e916b33d9e5 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -141,8 +141,9 @@ def run_test(self): f"-stopatheight={PAUSE_HEIGHT}", *self.extra_args[1]], expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) - # Finally connect the nodes and let them sync. - self.connect_nodes(0, 1) + # Finally connect the nodes and let them sync. Avoid a race between + # connection assertions and -stopatheight tripping. + self.connect_nodes(0, 1, wait_for_connect=False) n1.wait_until_stopped(timeout=5) @@ -156,7 +157,15 @@ def run_test(self): self.connect_nodes(0, 1) self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})") - self.wait_until(lambda: n1.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) + + def check_for_final_height(): + chainstates = n1.getchainstates() + # Background validation may complete before the first check, so + # accept the final height from either chainstate type. + cs = chainstates.get('snapshot') or chainstates.get('normal') + return cs['blocks'] == FINAL_HEIGHT + + self.wait_until(check_for_final_height) self.sync_blocks(nodes=(n0, n1)) self.log.info("Ensuring background validation completes") diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 93ff12af4f42..5070af1279b5 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -699,7 +699,14 @@ def restart_node(self, i, extra_args=None, expected_stderr=''): def wait_for_node_exit(self, i, timeout): self.nodes[i].process.wait(timeout) - def connect_nodes(self, a, b, *, peer_advertises_v2=None): + def connect_nodes(self, a, b, *, peer_advertises_v2=None, wait_for_connect: bool = True): + """ + Kwargs: + wait_for_connect: if True, block until the nodes are verified as connected. You might + want to disable this when using -stopatheight with one of the connected nodes, + since there will be a race between the actual connection and performing + the assertions before one node shuts down. + """ # A node cannot connect to itself, bail out early if (a == b): return @@ -718,6 +725,9 @@ def connect_nodes(self, a, b, *, peer_advertises_v2=None): # compatibility with older clients from_connection.addnode(ip_port, "onetry") + if not wait_for_connect: + return + # Use subversion as peer id. Test nodes have their node number appended to the user agent string from_connection_subver = from_connection.getnetworkinfo()['subversion'] to_connection_subver = to_connection.getnetworkinfo()['subversion'] @@ -1511,12 +1521,12 @@ def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, binary=Non # controller node is the only node that has an extra option allowing it to submit sporks append_config(self.nodes[0].datadir, ["sporkkey=cP4EKFyJsHT39LDqgdcB43Y3YXjNyjb5Fuas1GQSeAtjnZWmZEQK"]) - def connect_nodes(self, a, b, *, peer_advertises_v2=None): + def connect_nodes(self, a, b, *, peer_advertises_v2=None, wait_for_connect: bool = True): for mn2 in self.mninfo: # type: MasternodeInfo if mn2.nodeIdx is not None: mn2.get_node(self).setmnthreadactive(False) - super().connect_nodes(a, b, peer_advertises_v2=peer_advertises_v2) - for mn2 in self.mninfo: # type: MasternodeInfo + super().connect_nodes(a, b, peer_advertises_v2=peer_advertises_v2, wait_for_connect=wait_for_connect) + for mn2 in self.mninfo: if mn2.nodeIdx is not None: mn2.get_node(self).setmnthreadactive(True) From 4510ecc0ca536583535437b98eafba4e74ec9024 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 5 Oct 2023 14:05:57 -0400 Subject: [PATCH 07/30] Merge bitcoin/bitcoin#28590: assumeutxo: change getchainstates RPC to return a list of chainstates a9ef702a877a964bac724a56e2c0b5bee4ea7586 assumeutxo: change getchainstates RPC to return a list of chainstates (Ryan Ofsky) Pull request description: Current `getchainstates` RPC returns "normal" and "snapshot" fields which are not ideal because it requires new "normal" and "snapshot" terms to be defined, and the definitions are not really consistent with internal code. (In the RPC interface, the "snapshot" chainstate becomes the "normal" chainstate after it is validated, while in internal code there is no "normal chainstate" and the "snapshot chainstate" is still called that temporarily after it is validated). The current `getchainstates` RPC is also awkward to use if you to want information about the most-work chainstate, because you have to look at the "snapshot" field if it exists, and otherwise fall back to the "normal" field. Fix these issues by having `getchainstates` just return a flat list of chainstates ordered by work, and adding a new chainstate "validated" field alongside the existing "snapshot_blockhash" field so it is explicit if a chainstate was originally loaded from a snapshot, and whether the snapshot has been validated. This change was motivated by comment thread in https://github.com/bitcoin/bitcoin/pull/28562#discussion_r1344154808 ACKs for top commit: Sjors: re-ACK a9ef702a877a964bac724a56e2c0b5bee4ea7586 jamesob: re-ACK a9ef702 achow101: ACK a9ef702a877a964bac724a56e2c0b5bee4ea7586 Tree-SHA512: b364e2e96675fb7beaaee60c4dff4b69e6bc2d8a30dea1ba094265633d1cddf9dbf1c5ce20c07d6e23222cf1e92a195acf6227e4901f3962e81a1e53a43490aa --- src/rpc/blockchain.cpp | 22 ++++++-------- test/functional/feature_assumeutxo.py | 44 +++++++++++++++------------ 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 6fa2ed8955d9..54c0283b9998 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -3285,6 +3285,7 @@ const std::vector RPCHelpForChainstate{ {RPCResult::Type::STR_HEX, "snapshot_blockhash", /*optional=*/true, "the base block of the snapshot this chainstate is based on, if any"}, {RPCResult::Type::NUM, "coins_db_cache_bytes", "size of the coinsdb cache"}, {RPCResult::Type::NUM, "coins_tip_cache_bytes", "size of the coinstip cache"}, + {RPCResult::Type::BOOL, "validated", "whether the chainstate is fully validated. True if all blocks in the chainstate were validated, false if the chain is based on a snapshot and the snapshot has not yet been validated."}, }; static RPCHelpMan getchainstates() @@ -3296,8 +3297,7 @@ return RPCHelpMan{ RPCResult{ RPCResult::Type::OBJ, "", "", { {RPCResult::Type::NUM, "headers", "the number of headers seen so far"}, - {RPCResult::Type::OBJ, "normal", /*optional=*/true, "fully validated chainstate containing blocks this node has validated starting from the genesis block", RPCHelpForChainstate}, - {RPCResult::Type::OBJ, "snapshot", /*optional=*/true, "only present if an assumeutxo snapshot is loaded. Partially validated chainstate containing blocks this node has validated starting from the snapshot. After the snapshot is validated (when the 'normal' chainstate advances far enough to validate it), this chainstate will replace and become the 'normal' chainstate.", RPCHelpForChainstate}, + {RPCResult::Type::ARR, "chainstates", "list of the chainstates ordered by work, with the most-work (active) chainstate last", {{RPCResult::Type::OBJ, "", "", RPCHelpForChainstate},}}, } }, RPCExamples{ @@ -3311,7 +3311,7 @@ return RPCHelpMan{ ChainstateManager& chainman = EnsureAnyChainman(request.context); - auto make_chain_data = [&](const Chainstate& cs) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + auto make_chain_data = [&](const Chainstate& cs, bool validated) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); UniValue data(UniValue::VOBJ); if (!cs.m_chain.Tip()) { @@ -3329,20 +3329,18 @@ return RPCHelpMan{ if (cs.m_from_snapshot_blockhash) { data.pushKV("snapshot_blockhash", cs.m_from_snapshot_blockhash->ToString()); } + data.pushKV("validated", validated); return data; }; - if (chainman.GetAll().size() > 1) { - for (Chainstate* chainstate : chainman.GetAll()) { - obj.pushKV( - chainstate->m_from_snapshot_blockhash ? "snapshot" : "normal", - make_chain_data(*chainstate)); - } - } else { - obj.pushKV("normal", make_chain_data(chainman.ActiveChainstate())); - } obj.pushKV("headers", chainman.m_best_header ? chainman.m_best_header->nHeight : -1); + const auto& chainstates = chainman.GetAll(); + UniValue obj_chainstates{UniValue::VARR}; + for (Chainstate* cs : chainstates) { + obj_chainstates.push_back(make_chain_data(*cs, !cs->m_from_snapshot_blockhash || chainstates.size() == 1)); + } + obj.pushKV("chainstates", std::move(obj_chainstates)); return obj; } }; diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 5e916b33d9e5..214529caa042 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -127,10 +127,13 @@ def run_test(self): assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT) - monitor = n1.getchainstates() - assert_equal(monitor['normal']['blocks'], START_HEIGHT) - assert_equal(monitor['snapshot']['blocks'], SNAPSHOT_BASE_HEIGHT) - assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash']) + normal, snapshot = n1.getchainstates()["chainstates"] + assert_equal(normal['blocks'], START_HEIGHT) + assert_equal(normal.get('snapshot_blockhash'), None) + assert_equal(normal['validated'], True) + assert_equal(snapshot['blocks'], SNAPSHOT_BASE_HEIGHT) + assert_equal(snapshot['snapshot_blockhash'], dump_output['base_hash']) + assert_equal(snapshot['validated'], False) assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) @@ -159,18 +162,16 @@ def run_test(self): self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})") def check_for_final_height(): - chainstates = n1.getchainstates() + chainstates = n1.getchainstates()['chainstates'] # Background validation may complete before the first check, so - # accept the final height from either chainstate type. - cs = chainstates.get('snapshot') or chainstates.get('normal') - return cs['blocks'] == FINAL_HEIGHT + # accept the final height from either chainstate. + return any(cs['blocks'] == FINAL_HEIGHT for cs in chainstates) self.wait_until(check_for_final_height) self.sync_blocks(nodes=(n0, n1)) self.log.info("Ensuring background validation completes") - # N.B.: the `snapshot` key disappears once the background validation is complete. - self.wait_until(lambda: not n1.getchainstates().get('snapshot')) + self.wait_until(lambda: len(n1.getchainstates()['chainstates']) == 1) # Ensure indexes have synced. completed_idx_state = { @@ -188,8 +189,8 @@ def check_for_final_height(): assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT) - assert_equal(n.getchainstates()['normal']['blocks'], FINAL_HEIGHT) - assert_equal(n.getchainstates().get('snapshot'), None) + chainstate, = n.getchainstates()['chainstates'] + assert_equal(chainstate['blocks'], FINAL_HEIGHT) if i != 0: # Ensure indexes have synced for the assumeutxo node @@ -207,17 +208,20 @@ def check_for_final_height(): assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT) - monitor = n2.getchainstates() - assert_equal(monitor['normal']['blocks'], START_HEIGHT) - assert_equal(monitor['snapshot']['blocks'], SNAPSHOT_BASE_HEIGHT) - assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash']) + normal, snapshot = n2.getchainstates()['chainstates'] + assert_equal(normal['blocks'], START_HEIGHT) + assert_equal(normal.get('snapshot_blockhash'), None) + assert_equal(normal['validated'], True) + assert_equal(snapshot['blocks'], SNAPSHOT_BASE_HEIGHT) + assert_equal(snapshot['snapshot_blockhash'], dump_output['base_hash']) + assert_equal(snapshot['validated'], False) self.connect_nodes(0, 2) - self.wait_until(lambda: n2.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) + self.wait_until(lambda: n2.getchainstates()['chainstates'][-1]['blocks'] == FINAL_HEIGHT) self.sync_blocks() self.log.info("Ensuring background validation completes") - self.wait_until(lambda: not n2.getchainstates().get('snapshot')) + self.wait_until(lambda: len(n2.getchainstates()['chainstates']) == 1) completed_idx_state = { 'basic block filter index': COMPLETE_IDX, @@ -233,8 +237,8 @@ def check_for_final_height(): assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT) - assert_equal(n.getchainstates()['normal']['blocks'], FINAL_HEIGHT) - assert_equal(n.getchainstates().get('snapshot'), None) + chainstate, = n.getchainstates()['chainstates'] + assert_equal(chainstate['blocks'], FINAL_HEIGHT) if i != 0: # Ensure indexes have synced for the assumeutxo node From 2b2f10f0b9df8fb61dac4f52d8e31e1704a9a235 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Mon, 23 Oct 2023 12:40:29 -0400 Subject: [PATCH 08/30] Merge bitcoin/bitcoin#28618: doc: assumeutxo prune and index notes 03f82087f6ce1c29327f34d12945200494e6956d doc: assumeutxo prune and index notes (Sjors Provoost) Pull request description: Based on recent comments on #27596. ACKs for top commit: pablomartin4btc: re ACK 03f82087f6ce1c29327f34d12945200494e6956d ryanofsky: ACK 03f82087f6ce1c29327f34d12945200494e6956d. Nice changes, these seem like very helpful notes Tree-SHA512: fe651b49f4d667400a3655899f27a96dd1eaf67cf9215fb35db5f44fb8c0313e7d541518be6791fec93392df24b909793f3886adb808e53228ed2a291165639d --- doc/design/assumeutxo.md | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/doc/design/assumeutxo.md b/doc/design/assumeutxo.md index 573339395d9b..1ff6b3eae79c 100644 --- a/doc/design/assumeutxo.md +++ b/doc/design/assumeutxo.md @@ -3,8 +3,44 @@ Assumeutxo is a feature that allows fast bootstrapping of a validating dashd instance. -The RPC commands `dumptxoutset` and `loadtxoutset` are used to -respectively generate and load UTXO snapshots. The utility script +## Loading a snapshot + +There is currently no canonical source for snapshots, but any downloaded snapshot +will be checked against a hash that's been hardcoded in source code. + +Once you've obtained the snapshot, you can use the RPC command `loadtxoutset` to +load it. + +### Pruning + +A pruned node can load a snapshot. To save space, it's possible to delete the +snapshot file as soon as `loadtxoutset` finishes. + +The minimum `-dbcache` setting is 550 MiB, but this functionality ignores that +minimum and uses at least 1100 MiB. + +As the background sync continues there will be temporarily two chainstate +directories, each multiple gigabytes in size (likely growing larger than the +the downloaded snapshot). + +### Indexes + +Indexes work but don't take advantage of this feature. They always start building +from the genesis block. Once the background validation reaches the snapshot block, +indexes will continue to build all the way to the tip. + +For indexes that support pruning, note that no pruning will take place between +the snapshot and the tip, until the background sync has completed - after which +everything is pruned. Depending on how old the snapshot is, this may temporarily +use a significant amount of disk space. + +## Generating a snapshot + +The RPC command `dumptxoutset` can be used to generate a snapshot. This can be used +to create a snapshot on one node that you wish to load on another node. +It can also be used to verify the hardcoded snapshot hash in the source code. + +The utility script `./contrib/devtools/utxo_snapshot.sh` may be of use. ## General background From 28be4d50a1b451de015b283103f2e4f971716866 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 11 Oct 2023 14:19:22 -0400 Subject: [PATCH 09/30] Merge bitcoin/bitcoin#28625: test: check that loading snapshot not matching AssumeUTXO parameters fails 2e31250027ac580a7a72221fe2ff505b30836175 test: check that loading snapshot not matching AssumeUTXO parameters fails (Sebastian Falbesoner) Pull request description: This PR adds test coverage for the failed loading of an AssumeUTXO snapshot in case the referenced block hash doesn't match the parameters in the chainparams. Right now, I expect this would be the most common error-case for `loadtxoutset` out in the wild, as for mainnet the `m_assumeutxo_data` map is empty and this error condition would obviously always be triggered for any (otherwise valid, correctly encoded) snapshot. Note that this test-case is the simplest scenario and doesn't cover any of the TODO ideas mentioned at the top of the functional test yet. ACKs for top commit: jamesob: ACK https://github.com/bitcoin/bitcoin/pull/28625/commits/2e31250027ac580a7a72221fe2ff505b30836175 Sjors: utACK 2e31250027ac580a7a72221fe2ff505b30836175 achow101: ACK 2e31250027ac580a7a72221fe2ff505b30836175 Tree-SHA512: 8bcb2d525c95fbc95f87d3e978ad717d95bddb1ff67cbe7d3b06e4783f0f1ffba32b17ef451468c39c23bc1b3ef1150baa71148c145275c386f2d4822d790d39 --- test/functional/feature_assumeutxo.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 214529caa042..f8402ed5b420 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -37,7 +37,10 @@ """ from test_framework.test_framework import BitcoinTestFramework from test_framework.governance import EXPECTED_STDERR_NO_GOV_PRUNE -from test_framework.util import assert_equal +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) START_HEIGHT = 199 SNAPSHOT_BASE_HEIGHT = 299 @@ -63,6 +66,25 @@ def setup_network(self): self.add_nodes(3) self.start_nodes(extra_args=self.extra_args) + def test_invalid_snapshot_scenarios(self, valid_snapshot_path): + self.log.info("Test different scenarios of loading invalid snapshot files") + self.log.info(" - snapshot file refering to a block that is not in the assumeutxo parameters") + with open(valid_snapshot_path, 'rb') as f: + valid_snapshot_contents = f.read() + + # we can only test this with a block that is already known, as otherwise the `loadtxoutset` RPC + # would time out (waiting to see the hash in the headers chain), rather than error immediately + bad_snapshot_height = SNAPSHOT_BASE_HEIGHT - 1 + bad_snapshot_path = valid_snapshot_path + '.mod' + with open(bad_snapshot_path, 'wb') as f: + bad_snapshot_block_hash = self.nodes[0].getblockhash(bad_snapshot_height) + # block hash of the snapshot base is stored right at the start (first 32 bytes) + f.write(bytes.fromhex(bad_snapshot_block_hash)[::-1] + valid_snapshot_contents[32:]) + + expected_log = f"assumeutxo height in snapshot metadata not recognized ({bad_snapshot_height}) - refusing to load snapshot" + with self.nodes[1].assert_debug_log([expected_log]): + assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", self.nodes[1].loadtxoutset, bad_snapshot_path) + def run_test(self): """ Bring up two (disconnected) nodes, mine some new blocks on the first, @@ -122,6 +144,8 @@ def run_test(self): assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT) + self.test_invalid_snapshot_scenarios(dump_output['path']) + self.log.info(f"Loading snapshot into second node from {dump_output['path']}") loaded = n1.loadtxoutset(dump_output['path']) assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) From 67a6220af9298f68a840b4d059c3fe7d6d789c1a Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Tue, 17 Oct 2023 11:38:11 -0400 Subject: [PATCH 10/30] Merge bitcoin/bitcoin#28647: test: Add assumeutxo test for wrong hash fa685715663117955e9bb795cbf79ddbd3dfed19 test: Add assumeutxo test for wrong hash (MarcoFalke) Pull request description: Also: * Update test TODOs * Fix off-by-4 typo in test, remove `struct` import ACKs for top commit: fjahr: utACK fa685715663117955e9bb795cbf79ddbd3dfed19 theStack: Code-review re-ACK fa685715663117955e9bb795cbf79ddbd3dfed19 pablomartin4btc: re ACK fa685715663117955e9bb795cbf79ddbd3dfed19 ryanofsky: Code review ACK fa685715663117955e9bb795cbf79ddbd3dfed19 Tree-SHA512: 877653010efe4e20018827e8ec2801d036e1344457401f0c9e5d55907b817724201dd2e3f0f29505bbff619882c0c2cd731ecdcd209258bcefe11b86ff0205dd --- test/functional/feature_assumeutxo.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index f8402ed5b420..7310df7dee27 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2021 The Bitcoin Core developers +# Copyright (c) 2021-present The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test for assumeutxo, a means of quickly bootstrapping a node using @@ -17,8 +17,7 @@ Interesting test cases could be loading an assumeutxo snapshot file with: -- TODO: An invalid hash -- TODO: Valid hash but invalid snapshot file (bad coin height or truncated file or +- TODO: Valid hash but invalid snapshot file (bad coin height or bad other serialization) - TODO: Valid snapshot file, but referencing an unknown block - TODO: Valid snapshot file, but referencing a snapshot block that turns out to be @@ -85,6 +84,26 @@ def test_invalid_snapshot_scenarios(self, valid_snapshot_path): with self.nodes[1].assert_debug_log([expected_log]): assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", self.nodes[1].loadtxoutset, bad_snapshot_path) + self.log.info(" - snapshot file with wrong number of coins") + valid_num_coins = int.from_bytes(valid_snapshot_contents[32:32 + 8], "little") + for off in [-1, +1]: + with open(bad_snapshot_path, 'wb') as f: + f.write(valid_snapshot_contents[:32]) + f.write((valid_num_coins + off).to_bytes(8, "little")) + f.write(valid_snapshot_contents[32 + 8:]) + expected_log = f"bad evo section marker (or coins left over) after 298 coins" if off == -1 else f"bad snapshot format or truncated snapshot after deserializing 299 coins" + with self.nodes[1].assert_debug_log([expected_log]): + assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", self.nodes[1].loadtxoutset, bad_snapshot_path) + + self.log.info(" - snapshot file with wrong outpoint hash") + with open(bad_snapshot_path, "wb") as f: + f.write(valid_snapshot_contents[:(32 + 8)]) + f.write(b"\xff" * 32) + f.write(valid_snapshot_contents[(32 + 8 + 32):]) + expected_log = "[snapshot] bad snapshot content hash: expected 2618646eb7f9b17a1982e206f94e8feec3efb3b7e7e97ade16b658eef7636519, got " + with self.nodes[1].assert_debug_log([expected_log]): + assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", self.nodes[1].loadtxoutset, bad_snapshot_path) + def run_test(self): """ Bring up two (disconnected) nodes, mine some new blocks on the first, From 9549ddebd5cc9120c63d24203ec14372c971b18e Mon Sep 17 00:00:00 2001 From: fanquake Date: Tue, 17 Oct 2023 10:05:45 +0100 Subject: [PATCH 11/30] Merge bitcoin/bitcoin#28652: assumeutxo: fail early if snapshot block hash doesn't match AssumeUTXO parameters 9620cb449374f234f72c1a9e1bae3d4b8c0ff171 assumeutxo: fail early if snapshot block hash doesn't match AssumeUTXO parameters (Sebastian Falbesoner) Pull request description: Right now the `loadtxoutset` RPC call treats literally all files with a minimum size of 40 bytes (=size of metadata) as potential valid snapshot candidates and the waiting loop for seeing the metadata block hash in the headers chain is always entered, e.g.: ``` $ ./src/bitcoin-cli loadtxoutset ~/.vimrc bitcoind log: ... 2023-10-15T14:55:45Z [snapshot] waiting to see blockheader 626174207465730a7265626d756e207465730a656c62616e65207861746e7973 in headers chain before snapshot activation ... ``` There is no point in doing any further action though if we already know from the start that the UTXO snapshot loading won't be successful. This PR adds an assumeutxo parameter check immediately after the metadata is read in, so we can fail immediately on a mismatch: ``` $ ./src/bitcoin-cli loadtxoutset ~/.vimrc error code: -32603 error message: Unable to load UTXO snapshot, assumeutxo block hash in snapshot metadata not recognized (626174207465730a7265626d756e207465730a656c62616e 65207861746e7973) ``` This way, users who mistakenly try to load files that are not snapshots don't have to wait 10 minutes (=the block header waiting timeout) anymore to get a negative response. If a file is loaded which is a valid snapshot (referencing to an existing block hash), but one which doesn't match the parameters, the feedback is also faster, as we don't have to wait anymore to see the hash in the headers chain before getting an error. This is also partially fixes #28621. ACKs for top commit: maflcko: lgtm ACK 9620cb449374f234f72c1a9e1bae3d4b8c0ff171 ryanofsky: Code review ACK 9620cb449374f234f72c1a9e1bae3d4b8c0ff171. This should fix an annoyance and bad UX. pablomartin4btc: tACK 9620cb449374f234f72c1a9e1bae3d4b8c0ff171 Tree-SHA512: f88b865e9d46254858e57c024463f389cd9d8760a7cb30c190aa1723a931e159987dfc2263a733825d700fa612e7416691e4d8aab64058f1aeb0a7fa9233ac9c --- src/rpc/blockchain.cpp | 7 +++++-- test/functional/feature_assumeutxo.py | 11 +++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 54c0283b9998..080633cf2096 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -3215,6 +3215,7 @@ static RPCHelpMan loadtxoutset() "loadtxoutset is unavailable in masternode mode because active signing contexts cannot be rebound safely"); } + ChainstateManager& chainman = EnsureChainman(node); fs::path path{AbsPathForConfigVal(EnsureArgsman(node), fs::u8path(request.params[0].get_str()))}; FILE* file{fsbridge::fopen(path, "rb")}; @@ -3229,14 +3230,16 @@ static RPCHelpMan loadtxoutset() afile >> metadata; uint256 base_blockhash = metadata.m_base_blockhash; + if (!chainman.GetParams().AssumeutxoForBlockhash(base_blockhash).has_value()) { + throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf("Unable to load UTXO snapshot, " + "assumeutxo block hash in snapshot metadata not recognized (%s)", base_blockhash.ToString())); + } int max_secs_to_wait_for_headers = 60 * 10; CBlockIndex* snapshot_start_block = nullptr; LogPrintf("[snapshot] waiting to see blockheader %s in headers chain before snapshot activation\n", base_blockhash.ToString()); - ChainstateManager& chainman = *node.chainman; - while (max_secs_to_wait_for_headers > 0) { snapshot_start_block = WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(base_blockhash)); diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 7310df7dee27..6f56b1317dee 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -71,18 +71,13 @@ def test_invalid_snapshot_scenarios(self, valid_snapshot_path): with open(valid_snapshot_path, 'rb') as f: valid_snapshot_contents = f.read() - # we can only test this with a block that is already known, as otherwise the `loadtxoutset` RPC - # would time out (waiting to see the hash in the headers chain), rather than error immediately - bad_snapshot_height = SNAPSHOT_BASE_HEIGHT - 1 bad_snapshot_path = valid_snapshot_path + '.mod' + bad_snapshot_block_hash = self.nodes[0].getblockhash(SNAPSHOT_BASE_HEIGHT - 1) with open(bad_snapshot_path, 'wb') as f: - bad_snapshot_block_hash = self.nodes[0].getblockhash(bad_snapshot_height) # block hash of the snapshot base is stored right at the start (first 32 bytes) f.write(bytes.fromhex(bad_snapshot_block_hash)[::-1] + valid_snapshot_contents[32:]) - - expected_log = f"assumeutxo height in snapshot metadata not recognized ({bad_snapshot_height}) - refusing to load snapshot" - with self.nodes[1].assert_debug_log([expected_log]): - assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", self.nodes[1].loadtxoutset, bad_snapshot_path) + error_details = f"assumeutxo block hash in snapshot metadata not recognized ({bad_snapshot_block_hash})" + assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot, {error_details}", self.nodes[1].loadtxoutset, bad_snapshot_path) self.log.info(" - snapshot file with wrong number of coins") valid_num_coins = int.from_bytes(valid_snapshot_contents[32:32 + 8], "little") From 66de44a0ea30b0bd2563f8eb67858fc7436313ff Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Wed, 18 Oct 2023 08:15:31 -0400 Subject: [PATCH 12/30] Merge bitcoin/bitcoin#28666: test: assumeutxo file with unknown block hash 621db2f00486d8fd6f06dca91c1b95ce61f64bc4 test: assumeutxo file with unknown block hash (Fabian Jahr) Pull request description: Takes care of one of the open Todos in the assumeutxo functional test. Since an unknown block could be any hash, I simply chose one placeholder, it could also be a random string though. ACKs for top commit: maflcko: lgtm ACK 621db2f00486d8fd6f06dca91c1b95ce61f64bc4 pablomartin4btc: cr ACK 621db2f00486d8fd6f06dca91c1b95ce61f64bc4 theStack: ACK 621db2f00486d8fd6f06dca91c1b95ce61f64bc4 ryanofsky: Code review ACK 621db2f00486d8fd6f06dca91c1b95ce61f64bc4 Tree-SHA512: ee0438ce619f7348c6f88e39b0ea7ddddb8832956d9034ecc795c6033d5d905c09d11b7d0d5afc38231b2fd091ea7c1bd0a0be99d9c32c4e6357a25d76294142 --- test/functional/feature_assumeutxo.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 6f56b1317dee..9ac74c090f8d 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -19,7 +19,6 @@ - TODO: Valid hash but invalid snapshot file (bad coin height or bad other serialization) -- TODO: Valid snapshot file, but referencing an unknown block - TODO: Valid snapshot file, but referencing a snapshot block that turns out to be invalid, or has an invalid parent - TODO: Valid snapshot file and snapshot block, but the block is not on the @@ -72,12 +71,14 @@ def test_invalid_snapshot_scenarios(self, valid_snapshot_path): valid_snapshot_contents = f.read() bad_snapshot_path = valid_snapshot_path + '.mod' - bad_snapshot_block_hash = self.nodes[0].getblockhash(SNAPSHOT_BASE_HEIGHT - 1) - with open(bad_snapshot_path, 'wb') as f: - # block hash of the snapshot base is stored right at the start (first 32 bytes) - f.write(bytes.fromhex(bad_snapshot_block_hash)[::-1] + valid_snapshot_contents[32:]) - error_details = f"assumeutxo block hash in snapshot metadata not recognized ({bad_snapshot_block_hash})" - assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot, {error_details}", self.nodes[1].loadtxoutset, bad_snapshot_path) + prev_block_hash = self.nodes[0].getblockhash(SNAPSHOT_BASE_HEIGHT - 1) + bogus_block_hash = "0" * 64 # Represents any unknown block hash + for bad_block_hash in [bogus_block_hash, prev_block_hash]: + with open(bad_snapshot_path, 'wb') as f: + # block hash of the snapshot base is stored right at the start (first 32 bytes) + f.write(bytes.fromhex(bad_block_hash)[::-1] + valid_snapshot_contents[32:]) + error_details = f"assumeutxo block hash in snapshot metadata not recognized ({bad_block_hash})" + assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot, {error_details}", self.nodes[1].loadtxoutset, bad_snapshot_path) self.log.info(" - snapshot file with wrong number of coins") valid_num_coins = int.from_bytes(valid_snapshot_contents[32:32 + 8], "little") From 1182c8438c0ff2fc184373525e84ee1ed173309f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 20 Oct 2023 16:08:28 -0400 Subject: [PATCH 13/30] Merge bitcoin/bitcoin#28669: test: check assumeutxo file for changed outpoint index + de-duplications d3223685b1bb3fb4b4626d2afe4bf753e04f7b0a test: De-dublicate/optimize assumeutxo test for further extensions (Fabian Jahr) 0a576d62fe014f31d352f01873121e84e7971bc9 test: check au file with changed outpoint index (Fabian Jahr) Pull request description: Also doing some de-duplications. I kept the second commit separate for now as I am not 100% if this is overdoing it and makes it harder to reason about. But it also makes it easier to add more cases where we change more data. ACKs for top commit: maflcko: lgtm ACK d3223685b1bb3fb4b4626d2afe4bf753e04f7b0a achow101: ACK d3223685b1bb3fb4b4626d2afe4bf753e04f7b0a Tree-SHA512: be950a34b0ed50cb58459df47cff6513df19d834bf81815572cd26b10dee26e6f80866f0c44023cf246aafbbd256e62d23ce903e8b07fdff2297bc7065799bb8 --- test/functional/feature_assumeutxo.py | 34 ++++++++++++++++----------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 9ac74c090f8d..aaaad5eebbaf 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -71,14 +71,18 @@ def test_invalid_snapshot_scenarios(self, valid_snapshot_path): valid_snapshot_contents = f.read() bad_snapshot_path = valid_snapshot_path + '.mod' + + def expected_error(log_msg="", rpc_details=""): + with self.nodes[1].assert_debug_log([log_msg]): + assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", self.nodes[1].loadtxoutset, bad_snapshot_path) prev_block_hash = self.nodes[0].getblockhash(SNAPSHOT_BASE_HEIGHT - 1) bogus_block_hash = "0" * 64 # Represents any unknown block hash for bad_block_hash in [bogus_block_hash, prev_block_hash]: with open(bad_snapshot_path, 'wb') as f: # block hash of the snapshot base is stored right at the start (first 32 bytes) f.write(bytes.fromhex(bad_block_hash)[::-1] + valid_snapshot_contents[32:]) - error_details = f"assumeutxo block hash in snapshot metadata not recognized ({bad_block_hash})" - assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot, {error_details}", self.nodes[1].loadtxoutset, bad_snapshot_path) + error_details = f", assumeutxo block hash in snapshot metadata not recognized ({bad_block_hash})" + expected_error(rpc_details=error_details) self.log.info(" - snapshot file with wrong number of coins") valid_num_coins = int.from_bytes(valid_snapshot_contents[32:32 + 8], "little") @@ -87,18 +91,20 @@ def test_invalid_snapshot_scenarios(self, valid_snapshot_path): f.write(valid_snapshot_contents[:32]) f.write((valid_num_coins + off).to_bytes(8, "little")) f.write(valid_snapshot_contents[32 + 8:]) - expected_log = f"bad evo section marker (or coins left over) after 298 coins" if off == -1 else f"bad snapshot format or truncated snapshot after deserializing 299 coins" - with self.nodes[1].assert_debug_log([expected_log]): - assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", self.nodes[1].loadtxoutset, bad_snapshot_path) - - self.log.info(" - snapshot file with wrong outpoint hash") - with open(bad_snapshot_path, "wb") as f: - f.write(valid_snapshot_contents[:(32 + 8)]) - f.write(b"\xff" * 32) - f.write(valid_snapshot_contents[(32 + 8 + 32):]) - expected_log = "[snapshot] bad snapshot content hash: expected 2618646eb7f9b17a1982e206f94e8feec3efb3b7e7e97ade16b658eef7636519, got " - with self.nodes[1].assert_debug_log([expected_log]): - assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", self.nodes[1].loadtxoutset, bad_snapshot_path) + expected_error(log_msg=f"bad evo section marker (or coins left over) after 298 coins" if off == -1 else f"bad snapshot format or truncated snapshot after deserializing 299 coins") + + self.log.info(" - snapshot file with alternated UTXO data") + cases = [ + [b"\xff" * 32, 0, "997cb8178bca9287d202a1591040a6473b0a89b7d09dcb77f84cbd9b9d45ade3"], # wrong outpoint hash + [(1).to_bytes(4, "little"), 32, "4baca1d9c065ca6eade452029615dcb68d3c9683f7900cfe4cd9baa19d6b088c"], # wrong outpoint index + ] + + for content, offset, wrong_hash in cases: + with open(bad_snapshot_path, "wb") as f: + f.write(valid_snapshot_contents[:(32 + 8 + offset)]) + f.write(content) + f.write(valid_snapshot_contents[(32 + 8 + offset + len(content)):]) + expected_error(log_msg=f"[snapshot] bad snapshot content hash: expected 2618646eb7f9b17a1982e206f94e8feec3efb3b7e7e97ade16b658eef7636519, got {wrong_hash}") def run_test(self): """ From 3da26dc1e959ec1e28221a5e46192479daeb98f1 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 23 Oct 2023 15:05:16 -0400 Subject: [PATCH 14/30] Merge bitcoin/bitcoin#28685: coinstats, assumeutxo: fix hash_serialized2 calculation Regenerate Dash regtest AssumeutxoData commitments after switching to the corrected per-coin hash_serialized_3 serialization. Procedure: build the post-change dashd/test_dash; run validation_chainstatemanager_tests snapshot activation cases for heights 110 and 200; run feature_assumeutxo.py to dump the deterministic height-299 snapshot; run rpc_dumptxoutset.py and feature_utxo_set_hash.py to refresh their deterministic golden values. Independently decode the preserved snapshot metadata/coins and hash outpoint || uint32_le((height << 1) + coinbase) || CTxOut with SHA256d to cross-check the three AssumeutxoData values. AssumeutxoData hash_serialized changes: height 110 9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b -> ffb210087e1ed14526c0c08a3ec3a7c8e288079eaa68acb87d3d4d9fd746079f; height 200 8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3 -> 16e00a64db4fa48dd989dce86d8677f41797d52044e5fc86021aa88cc22b665b; height 299 2618646eb7f9b17a1982e206f94e8feec3efb3b7e7e97ade16b658eef7636519 -> d7f46f9830ea11f1bfc565b08f63b66f09e1403b54c988ede40461cf0846fcba. Also refresh rpc_dumptxoutset txoutset_hash to 3baec1b74f1f02749a2b519655f84326aa903ed00bca049950efffb927ac2b1d, its deterministic snapshot-file checksum from e8b1b739921ea48cc87021cd47adc711bf864c28fe82a0d1b3a139f32fe377b3 to 3ee2d4e678f0bcb73e28648434e4b32b5f6ffa600625905ad18fae2a02f42b26, and feature_utxo_set_hash to 8d439ff1e8fdffcdc5006e9b50c9ec5a33cf839313ab63401e6862a9c48bf093. evo_hash is unaffected and remains f2ccd3fef604df58a0c174489821e16912c9332969a267650cd040e85fb2adde at height 299. Verified that evo::GetEvoSnapshotHash hashes canonical CEvoSnapshot serialization with its own CSHA256 path and does not use coinstats TxOutSer, ApplyCoinHash, or ComputeUTXOStats. --- contrib/devtools/utxo_snapshot.sh | 2 +- doc/release-notes-28685.md | 4 ++ src/chainparams.cpp | 6 +-- src/index/coinstatsindex.cpp | 11 ++-- src/kernel/coinstats.cpp | 65 ++++++++++------------- src/kernel/coinstats.h | 4 +- src/rpc/blockchain.cpp | 10 ++-- src/test/validation_tests.cpp | 6 +-- src/validation.cpp | 5 ++ test/functional/feature_assumeutxo.py | 10 ++-- test/functional/feature_coinstatsindex.py | 6 +-- test/functional/feature_dbcrash.py | 8 +-- test/functional/feature_utxo_set_hash.py | 2 +- test/functional/rpc_blockchain.py | 12 ++--- test/functional/rpc_dumptxoutset.py | 2 +- 15 files changed, 79 insertions(+), 74 deletions(-) create mode 100644 doc/release-notes-28685.md diff --git a/contrib/devtools/utxo_snapshot.sh b/contrib/devtools/utxo_snapshot.sh index 2d8583f9657c..c321c531e5be 100755 --- a/contrib/devtools/utxo_snapshot.sh +++ b/contrib/devtools/utxo_snapshot.sh @@ -97,7 +97,7 @@ ${BITCOIN_CLI_CALL} invalidateblock "${PIVOT_BLOCKHASH}" if [[ "${OUTPUT_PATH}" = "-" ]]; then (>&2 echo "Generating txoutset info...") - ${BITCOIN_CLI_CALL} gettxoutsetinfo | grep hash_serialized_2 | sed 's/^.*: "\(.\+\)\+",/\1/g' + ${BITCOIN_CLI_CALL} gettxoutsetinfo | grep hash_serialized_3 | sed 's/^.*: "\(.\+\)\+",/\1/g' else (>&2 echo "Generating UTXO snapshot...") ${BITCOIN_CLI_CALL} dumptxoutset "${OUTPUT_PATH}" diff --git a/doc/release-notes-28685.md b/doc/release-notes-28685.md new file mode 100644 index 000000000000..6f04d8d542b3 --- /dev/null +++ b/doc/release-notes-28685.md @@ -0,0 +1,4 @@ +RPC +--- + +The `hash_serialized_2` value has been removed from `gettxoutsetinfo` since the value it calculated contained a bug and did not take all data into account. It is superseded by `hash_serialized_3` which provides the same functionality but serves the correctly calculated hash. diff --git a/src/chainparams.cpp b/src/chainparams.cpp index a8ee1e37dab5..f3b7682a2002 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -882,7 +882,7 @@ class CRegTestParams : public CChainParams { m_assumeutxo_data = { { .height = 110, - .hash_serialized = AssumeutxoHash{uint256S("0x9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b")}, + .hash_serialized = AssumeutxoHash{uint256S("0xffb210087e1ed14526c0c08a3ec3a7c8e288079eaa68acb87d3d4d9fd746079f")}, // Unit-test chains at this height may have different empty evo // state encodings, so retain the regtest-only M4 wildcard. .evo_hash = EvoSnapshotHash{uint256{}}, @@ -891,7 +891,7 @@ class CRegTestParams : public CChainParams { }, { .height = 200, - .hash_serialized = AssumeutxoHash{uint256S("0x8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3")}, + .hash_serialized = AssumeutxoHash{uint256S("0x16e00a64db4fa48dd989dce86d8677f41797d52044e5fc86021aa88cc22b665b")}, .evo_hash = EvoSnapshotHash{uint256{}}, .nChainTx = 201, .blockhash = uint256S("0x19c1b203b5a960c7f3619e0e805b24c94e684b1aa261f3a79c84d291638a6e1f"), @@ -900,7 +900,7 @@ class CRegTestParams : public CChainParams { // For use by test/functional/feature_assumeutxo.py. Dash-specific // pre-DIP3 snapshot has an empty, but canonically serialized, evo section. .height = 299, - .hash_serialized = AssumeutxoHash{uint256S("0x2618646eb7f9b17a1982e206f94e8feec3efb3b7e7e97ade16b658eef7636519")}, + .hash_serialized = AssumeutxoHash{uint256S("0xd7f46f9830ea11f1bfc565b08f63b66f09e1403b54c988ede40461cf0846fcba")}, .evo_hash = EvoSnapshotHash{uint256S("0xf2ccd3fef604df58a0c174489821e16912c9332969a267650cd040e85fb2adde")}, .nChainTx = 300, .blockhash = uint256S("0x64ce3ab60754c7974ea472221fa9c7a04f2d193ba480d1ef61b5d12216b14760"), diff --git a/src/index/coinstatsindex.cpp b/src/index/coinstatsindex.cpp index 59c9cf99a863..b06ab50c0b2f 100644 --- a/src/index/coinstatsindex.cpp +++ b/src/index/coinstatsindex.cpp @@ -15,9 +15,10 @@ #include #include +using kernel::ApplyCoinHash; using kernel::CCoinsStats; using kernel::GetBogoSize; -using kernel::TxOutSer; +using kernel::RemoveCoinHash; using node::ReadBlockFromDisk; using node::UndoReadFromDisk; @@ -163,7 +164,7 @@ bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block) continue; } - m_muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin))); + ApplyCoinHash(m_muhash, outpoint, coin); if (tx->IsCoinBase()) { m_total_coinbase_amount += coin.out.nValue; @@ -184,7 +185,7 @@ bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block) Coin coin{tx_undo.vprevout[j]}; COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n}; - m_muhash.Remove(MakeUCharSpan(TxOutSer(outpoint, coin))); + RemoveCoinHash(m_muhash, outpoint, coin); m_total_prevout_spent_amount += coin.out.nValue; @@ -439,7 +440,7 @@ bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex continue; } - m_muhash.Remove(MakeUCharSpan(TxOutSer(outpoint, coin))); + RemoveCoinHash(m_muhash, outpoint, coin); if (tx->IsCoinBase()) { m_total_coinbase_amount -= coin.out.nValue; @@ -460,7 +461,7 @@ bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex Coin coin{tx_undo.vprevout[j]}; COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n}; - m_muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin))); + ApplyCoinHash(m_muhash, outpoint, coin); m_total_prevout_spent_amount -= coin.out.nValue; diff --git a/src/kernel/coinstats.cpp b/src/kernel/coinstats.cpp index 3da24b12186a..1d7e90a41464 100644 --- a/src/kernel/coinstats.cpp +++ b/src/kernel/coinstats.cpp @@ -48,14 +48,35 @@ uint64_t GetBogoSize(const CScript& script_pub_key) script_pub_key.size() /* scriptPubKey */; } -CDataStream TxOutSer(const COutPoint& outpoint, const Coin& coin) { - CDataStream ss(SER_DISK, PROTOCOL_VERSION); +template +static void TxOutSer(T& ss, const COutPoint& outpoint, const Coin& coin) +{ ss << outpoint; - ss << static_cast(coin.nHeight * 2 + coin.fCoinBase); + ss << static_cast((coin.nHeight << 1) + coin.fCoinBase); ss << coin.out; - return ss; } +static void ApplyCoinHash(HashWriter& ss, const COutPoint& outpoint, const Coin& coin) +{ + TxOutSer(ss, outpoint, coin); +} + +void ApplyCoinHash(MuHash3072& muhash, const COutPoint& outpoint, const Coin& coin) +{ + DataStream ss{}; + TxOutSer(ss, outpoint, coin); + muhash.Insert(MakeUCharSpan(ss)); +} + +void RemoveCoinHash(MuHash3072& muhash, const COutPoint& outpoint, const Coin& coin) +{ + DataStream ss{}; + TxOutSer(ss, outpoint, coin); + muhash.Remove(MakeUCharSpan(ss)); +} + +static void ApplyCoinHash(std::nullptr_t, const COutPoint& outpoint, const Coin& coin) {} + //! Warning: be very careful when changing this! assumeutxo and UTXO snapshot //! validation commitments are reliant on the hash constructed by this //! function. @@ -68,32 +89,13 @@ CDataStream TxOutSer(const COutPoint& outpoint, const Coin& coin) { //! It is also possible, though very unlikely, that a change in this //! construction could cause a previously invalid (and potentially malicious) //! UTXO snapshot to be considered valid. -static void ApplyHash(HashWriter& ss, const uint256& hash, const std::map& outputs) -{ - for (auto it = outputs.begin(); it != outputs.end(); ++it) { - if (it == outputs.begin()) { - ss << hash; - ss << VARINT(it->second.nHeight * 2 + it->second.fCoinBase ? 1u : 0u); - } - - ss << VARINT(it->first + 1); - ss << it->second.out.scriptPubKey; - ss << VARINT_MODE(it->second.out.nValue, VarIntMode::NONNEGATIVE_SIGNED); - - if (it == std::prev(outputs.end())) { - ss << VARINT(0u); - } - } -} - -static void ApplyHash(std::nullptr_t, const uint256& hash, const std::map& outputs) {} - -static void ApplyHash(MuHash3072& muhash, const uint256& hash, const std::map& outputs) +template +static void ApplyHash(T& hash_obj, const uint256& hash, const std::map& outputs) { for (auto it = outputs.begin(); it != outputs.end(); ++it) { COutPoint outpoint = COutPoint(hash, it->first); Coin coin = it->second; - muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin))); + ApplyCoinHash(hash_obj, outpoint, coin); } } @@ -117,8 +119,6 @@ static bool ComputeUTXOStats(CCoinsView* view, CCoinsStats& stats, T hash_obj, c std::unique_ptr pcursor(view->Cursor()); assert(pcursor); - PrepareHash(hash_obj, stats); - uint256 prevkey; std::map outputs; while (pcursor->Valid()) { @@ -179,15 +179,6 @@ std::optional ComputeUTXOStats(CoinStatsHashType hash_type, CCoinsV return stats; } -// The legacy hash serializes the hashBlock -static void PrepareHash(HashWriter& ss, const CCoinsStats& stats) -{ - ss << stats.hashBlock; -} -// MuHash does not need the prepare step -static void PrepareHash(MuHash3072& muhash, CCoinsStats& stats) {} -static void PrepareHash(std::nullptr_t, CCoinsStats& stats) {} - static void FinalizeHash(HashWriter& ss, CCoinsStats& stats) { stats.hashSerialized = ss.GetHash(); diff --git a/src/kernel/coinstats.h b/src/kernel/coinstats.h index 1750e8a78ba3..0d74516a6471 100644 --- a/src/kernel/coinstats.h +++ b/src/kernel/coinstats.h @@ -6,6 +6,7 @@ #define BITCOIN_KERNEL_COINSTATS_H #include +#include #include #include @@ -72,7 +73,8 @@ struct CCoinsStats { uint64_t GetBogoSize(const CScript& script_pub_key); -CDataStream TxOutSer(const COutPoint& outpoint, const Coin& coin); +void ApplyCoinHash(MuHash3072& muhash, const COutPoint& outpoint, const Coin& coin); +void RemoveCoinHash(MuHash3072& muhash, const COutPoint& outpoint, const Coin& coin); std::optional ComputeUTXOStats(CoinStatsHashType hash_type, CCoinsView* view, node::BlockManager& blockman, const std::function& interruption_point = {}); } // namespace kernel diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 080633cf2096..3be02ea8aeae 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1140,7 +1140,7 @@ static RPCHelpMan pruneblockchain() CoinStatsHashType ParseHashType(const std::string& hash_type_input) { - if (hash_type_input == "hash_serialized_2") { + if (hash_type_input == "hash_serialized_3") { return CoinStatsHashType::HASH_SERIALIZED; } else if (hash_type_input == "muhash") { return CoinStatsHashType::MUHASH; @@ -1182,7 +1182,7 @@ static RPCHelpMan gettxoutsetinfo() "\nReturns statistics about the unspent transaction output set.\n" "Note this call may take some time if you are not using coinstatsindex.\n", { - {"hash_type", RPCArg::Type::STR, RPCArg::Default{"hash_serialized_2"}, "Which UTXO set hash should be calculated. Options: 'hash_serialized_2' (the legacy algorithm), 'muhash', 'none'."}, + {"hash_type", RPCArg::Type::STR, RPCArg::Default{"hash_serialized_3"}, "Which UTXO set hash should be calculated. Options: 'hash_serialized_3' (the legacy algorithm), 'muhash', 'none'."}, {"hash_or_height", RPCArg::Type::NUM, RPCArg::DefaultHint{"the current best block"}, "The block hash or height of the target height (only available with coinstatsindex).", RPCArgOptions{.type_str={"", "string or numeric"}}}, {"use_index", RPCArg::Type::BOOL, RPCArg::Default{true}, "Use coinstatsindex, if available."}, }, @@ -1193,7 +1193,7 @@ static RPCHelpMan gettxoutsetinfo() {RPCResult::Type::STR_HEX, "bestblock", "The hash of the block at which these statistics are calculated"}, {RPCResult::Type::NUM, "txouts", "The number of unspent transaction outputs"}, {RPCResult::Type::NUM, "bogosize", "Database-independent, meaningless metric indicating the UTXO set size"}, - {RPCResult::Type::STR_HEX, "hash_serialized_2", /*optional=*/true, "The serialized hash (only present if 'hash_serialized_2' hash_type is chosen)"}, + {RPCResult::Type::STR_HEX, "hash_serialized_3", /*optional=*/true, "The serialized hash (only present if 'hash_serialized_3' hash_type is chosen)"}, {RPCResult::Type::STR_HEX, "muhash", /*optional=*/true, "The serialized hash (only present if 'muhash' hash_type is chosen)"}, {RPCResult::Type::NUM, "transactions", /*optional=*/true, "The number of transactions with unspent outputs (not available when coinstatsindex is used)"}, {RPCResult::Type::NUM, "disk_size", /*optional=*/true, "The estimated size of the chainstate on disk (not available when coinstatsindex is used)"}, @@ -1252,7 +1252,7 @@ static RPCHelpMan gettxoutsetinfo() } if (hash_type == CoinStatsHashType::HASH_SERIALIZED) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "hash_serialized_2 hash type cannot be queried for a specific block"); + throw JSONRPCError(RPC_INVALID_PARAMETER, "hash_serialized_3 hash type cannot be queried for a specific block"); } if (!index_requested) { @@ -1281,7 +1281,7 @@ static RPCHelpMan gettxoutsetinfo() ret.pushKV("txouts", (int64_t)stats.nTransactionOutputs); ret.pushKV("bogosize", (int64_t)stats.nBogoSize); if (hash_type == CoinStatsHashType::HASH_SERIALIZED) { - ret.pushKV("hash_serialized_2", stats.hashSerialized.GetHex()); + ret.pushKV("hash_serialized_3", stats.hashSerialized.GetHex()); } if (hash_type == CoinStatsHashType::MUHASH) { ret.pushKV("muhash", stats.hashSerialized.GetHex()); diff --git a/src/test/validation_tests.cpp b/src/test/validation_tests.cpp index ab8f9bcafb92..a8fc2d6c3069 100644 --- a/src/test/validation_tests.cpp +++ b/src/test/validation_tests.cpp @@ -32,15 +32,15 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo) } const auto out110 = *params->AssumeutxoForHeight(110); - BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b"); + BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "ffb210087e1ed14526c0c08a3ec3a7c8e288079eaa68acb87d3d4d9fd746079f"); BOOST_CHECK_EQUAL(out110.nChainTx, 111U); const auto out110_2 = *params->AssumeutxoForBlockhash(uint256S("0x729bcb1479ff9f4968439f0276bd76bcb2de0f0720b7a16f383321f6a41cb238")); - BOOST_CHECK_EQUAL(out110_2.hash_serialized.ToString(), "9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b"); + BOOST_CHECK_EQUAL(out110_2.hash_serialized.ToString(), "ffb210087e1ed14526c0c08a3ec3a7c8e288079eaa68acb87d3d4d9fd746079f"); BOOST_CHECK_EQUAL(out110_2.nChainTx, 111U); const auto out210 = *params->AssumeutxoForHeight(200); - BOOST_CHECK_EQUAL(out210.hash_serialized.ToString(), "8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3"); + BOOST_CHECK_EQUAL(out210.hash_serialized.ToString(), "16e00a64db4fa48dd989dce86d8677f41797d52044e5fc86021aa88cc22b665b"); BOOST_CHECK_EQUAL(out210.nChainTx, 201U); } diff --git a/src/validation.cpp b/src/validation.cpp index 32eb0932aa9c..a027c95fa7c7 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5881,6 +5881,11 @@ bool ChainstateManager::PopulateAndValidateSnapshot( coins_count - coins_left); return false; } + if (!MoneyRange(coin.out.nValue)) { + LogPrintf("[snapshot] bad snapshot data after deserializing %d coins - bad tx out value\n", + coins_count - coins_left); + return false; + } coins_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin)); diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index aaaad5eebbaf..48531c65718c 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -95,8 +95,10 @@ def expected_error(log_msg="", rpc_details=""): self.log.info(" - snapshot file with alternated UTXO data") cases = [ - [b"\xff" * 32, 0, "997cb8178bca9287d202a1591040a6473b0a89b7d09dcb77f84cbd9b9d45ade3"], # wrong outpoint hash - [(1).to_bytes(4, "little"), 32, "4baca1d9c065ca6eade452029615dcb68d3c9683f7900cfe4cd9baa19d6b088c"], # wrong outpoint index + [b"\xff" * 32, 0, "91dbd69333f61346aa0d711f37f4d1801a9932572c8671d1f2367ba3e62688d0"], # wrong outpoint hash + [(1).to_bytes(4, "little"), 32, "fd2cfc6c6acecf6f3e8d2cc22ed853defcc369dea35f02a3472beffa2a5945a9"], # wrong outpoint index + [b"\x82", 36, "16c0fc9c9c9a2814513dd2fdcd81b48692ea5684365f56a5e76e8394d6439c16"], # wrong coin code VARINT((coinbase ? 1 : 0) | (height << 1)) + [b"\x83", 36, "6f0c228f8ceae88a7bac7c1384897d67f8b6f9d55b12f9f2da50f0446c549986"], # another wrong coin code ] for content, offset, wrong_hash in cases: @@ -104,7 +106,7 @@ def expected_error(log_msg="", rpc_details=""): f.write(valid_snapshot_contents[:(32 + 8 + offset)]) f.write(content) f.write(valid_snapshot_contents[(32 + 8 + offset + len(content)):]) - expected_error(log_msg=f"[snapshot] bad snapshot content hash: expected 2618646eb7f9b17a1982e206f94e8feec3efb3b7e7e97ade16b658eef7636519, got {wrong_hash}") + expected_error(log_msg=f"[snapshot] bad snapshot content hash: expected d7f46f9830ea11f1bfc565b08f63b66f09e1403b54c988ede40461cf0846fcba, got {wrong_hash}") def run_test(self): """ @@ -151,7 +153,7 @@ def run_test(self): assert_equal( dump_output['txoutset_hash'], - '2618646eb7f9b17a1982e206f94e8feec3efb3b7e7e97ade16b658eef7636519') + 'd7f46f9830ea11f1bfc565b08f63b66f09e1403b54c988ede40461cf0846fcba') assert_equal(dump_output['evo_hash'], 'f2ccd3fef604df58a0c174489821e16912c9332969a267650cd040e85fb2adde') assert_equal(dump_output['nchaintx'], 300) assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) diff --git a/test/functional/feature_coinstatsindex.py b/test/functional/feature_coinstatsindex.py index b27f1b0b4759..e32cf0d747cf 100755 --- a/test/functional/feature_coinstatsindex.py +++ b/test/functional/feature_coinstatsindex.py @@ -303,11 +303,11 @@ def _test_reorg_index(self): def _test_index_rejects_hash_serialized(self): self.log.info("Test that the rpc raises if the legacy hash is passed with the index") - msg = "hash_serialized_2 hash type cannot be queried for a specific block" - assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_2', hash_or_height=111) + msg = "hash_serialized_3 hash type cannot be queried for a specific block" + assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_3', hash_or_height=111) for use_index in {True, False, None}: - assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_2', hash_or_height=111, use_index=use_index) + assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_3', hash_or_height=111, use_index=use_index) if __name__ == '__main__': diff --git a/test/functional/feature_dbcrash.py b/test/functional/feature_dbcrash.py index c0f1880169f4..9dc7c5d56583 100755 --- a/test/functional/feature_dbcrash.py +++ b/test/functional/feature_dbcrash.py @@ -83,7 +83,7 @@ def restart_node(self, node_index, expected_tip): # Any of these RPC calls could throw due to node crash self.start_node(node_index) self.nodes[node_index].waitforblock(expected_tip) - utxo_hash = self.nodes[node_index].gettxoutsetinfo()['hash_serialized_2'] + utxo_hash = self.nodes[node_index].gettxoutsetinfo()['hash_serialized_3'] return utxo_hash except Exception: # An exception here should mean the node is about to crash. @@ -128,7 +128,7 @@ def sync_node3blocks(self, block_hashes): If any nodes crash while updating, we'll compare utxo hashes to ensure recovery was successful.""" - node3_utxo_hash = self.nodes[3].gettxoutsetinfo()['hash_serialized_2'] + node3_utxo_hash = self.nodes[3].gettxoutsetinfo()['hash_serialized_3'] # Retrieve all the blocks from node3 blocks = [] @@ -170,12 +170,12 @@ def verify_utxo_hash(self): """Verify that the utxo hash of each node matches node3. Restart any nodes that crash while querying.""" - node3_utxo_hash = self.nodes[3].gettxoutsetinfo()['hash_serialized_2'] + node3_utxo_hash = self.nodes[3].gettxoutsetinfo()['hash_serialized_3'] self.log.info("Verifying utxo hash matches for all nodes") for i in range(3): try: - nodei_utxo_hash = self.nodes[i].gettxoutsetinfo()['hash_serialized_2'] + nodei_utxo_hash = self.nodes[i].gettxoutsetinfo()['hash_serialized_3'] except OSError: # probably a crash on db flushing nodei_utxo_hash = self.restart_node(i, self.nodes[3].getbestblockhash()) diff --git a/test/functional/feature_utxo_set_hash.py b/test/functional/feature_utxo_set_hash.py index 58ab77fdb6cb..742d684c80c0 100755 --- a/test/functional/feature_utxo_set_hash.py +++ b/test/functional/feature_utxo_set_hash.py @@ -67,7 +67,7 @@ def test_muhash_implementation(self): assert_equal(finalized[::-1].hex(), node_muhash) self.log.info("Test deterministic UTXO set hash results") - assert_equal(node.gettxoutsetinfo()['hash_serialized_2'], "1d640be368b1d811619bc27bc435db672e3ce17f524c0f78def9f7398aef9eac") + assert_equal(node.gettxoutsetinfo()['hash_serialized_3'], "8d439ff1e8fdffcdc5006e9b50c9ec5a33cf839313ab63401e6862a9c48bf093") assert_equal(node.gettxoutsetinfo("muhash")['muhash'], "c2ca3b5233cf7f4f9ba63ecd3166abf9a6b8c76fe050cf7d51acaeca0d52e78e") def run_test(self): diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 3559690e3fec..a141187da609 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -359,7 +359,7 @@ def _test_gettxoutsetinfo(self): assert size > 6400 assert size < 64000 assert_equal(len(res['bestblock']), 64) - assert_equal(len(res['hash_serialized_2']), 64) + assert_equal(len(res['hash_serialized_3']), 64) self.log.info("Test gettxoutsetinfo works for blockchain with just the genesis block") b1hash = node.getblockhash(1) @@ -372,7 +372,7 @@ def _test_gettxoutsetinfo(self): assert_equal(res2['txouts'], 0) assert_equal(res2['bogosize'], 0), assert_equal(res2['bestblock'], node.getblockhash(0)) - assert_equal(len(res2['hash_serialized_2']), 64) + assert_equal(len(res2['hash_serialized_3']), 64) self.log.info("Test gettxoutsetinfo returns the same result after invalidate/reconsider block") node.reconsiderblock(b1hash) @@ -384,20 +384,20 @@ def _test_gettxoutsetinfo(self): assert_equal(res, res3) self.log.info("Test gettxoutsetinfo hash_type option") - # Adding hash_type 'hash_serialized_2', which is the default, should + # Adding hash_type 'hash_serialized_3', which is the default, should # not change the result. - res4 = node.gettxoutsetinfo(hash_type='hash_serialized_2') + res4 = node.gettxoutsetinfo(hash_type='hash_serialized_3') del res4['disk_size'] assert_equal(res, res4) # hash_type none should not return a UTXO set hash. res5 = node.gettxoutsetinfo(hash_type='none') - assert 'hash_serialized_2' not in res5 + assert 'hash_serialized_3' not in res5 # hash_type muhash should return a different UTXO set hash. res6 = node.gettxoutsetinfo(hash_type='muhash') assert 'muhash' in res6 - assert res['hash_serialized_2'] != res6['muhash'] + assert res['hash_serialized_3'] != res6['muhash'] # muhash should not be returned unless requested. for r in [res, res2, res3, res4, res5]: diff --git a/test/functional/rpc_dumptxoutset.py b/test/functional/rpc_dumptxoutset.py index 0dcb60dbed9e..9730906229a6 100755 --- a/test/functional/rpc_dumptxoutset.py +++ b/test/functional/rpc_dumptxoutset.py @@ -49,7 +49,7 @@ def run_test(self): assert b'DASHEVO\x00' in expected_path.read_bytes() assert_equal( - out['txoutset_hash'], 'b2d7429106c96f5ab831843d5c96ba131ca8793111d0a0e30e7d7d8b4841e6cc') + out['txoutset_hash'], '3baec1b74f1f02749a2b519655f84326aa903ed00bca049950efffb927ac2b1d') assert_equal(out['nchaintx'], 101) assert_equal(out['evo_mn_count'], 0) assert_equal(len(out['evo_hash']), 64) From 9b4d4f9be728f5357a7919019bec744deff74552 Mon Sep 17 00:00:00 2001 From: fanquake Date: Sun, 29 Oct 2023 10:21:51 +0100 Subject: [PATCH 15/30] Merge bitcoin/bitcoin#28698: assumeutxo, blockstorage: Prevent core dump on invalid hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 811067ca1cbbd4a697791cbe3ecd4bee19fe6193 test: add coverage for snapshot chainstate not matching AssumeUTXO parameters (pablomartin4btc) 4a5be10b928d4ed33d223972537c1cb79163e79c assumeutxo, blockstorage: prevent core dump on invalid hash (pablomartin4btc) Pull request description: While reviewing #27596 (ran `loadtxoutset` in `mainnet` before `m_assumeutxo_data` is empty as [currently](https://github.com/jamesob/bitcoin/blob/434495a8c1496ca23fe35b84499f3daf668d76b8/src/kernel/chainparams.cpp#L175-L177) in master - back to 1b1d711), got a `core dumped`, so it seems there's a potential issue if new releases ever remove snapshot details or a semi-experienced user performs a `loadtxoutset` on a different "customised" binary version (not sure if this is a real use case). ``` 2023-10-18T17:42:52Z [init] Using obfuscation key for /tmp/.test_utxo_2/blocks/index: 0000000000000000 node/blockstorage.cpp:390 LoadBlockIndex: Assertion `GetParams().AssumeutxoForBlockhash(*snapshot_blockhash)' failed. Aborted (core dumped) ```
This is also happening before IBD is completed (background validation still being performed as it can be seen in rpc getchainstates) ``` /src/bitcoin-cli -datadir=${AU_DATADIR} getchainstates { "headers": 813097, "chainstates": [ { "blocks": 368249, "bestblockhash": "00000000000000000b7a08224a1cb00d337100ba7a46c03d04b2c2d8964efc37", "difficulty": 52278304845.59168, "verificationprogress": 0.086288278873286, "coins_db_cache_bytes": 7969177, "coins_tip_cache_bytes": 14908338995, "validated": true }, { "blocks": 813097, "bestblockhash": "0000000000000000000270c9fdce7b17db64cca91f90106964b58e33a4d91089", "difficulty": 61030681983175.59, "verificationprogress": 0.999997140098457, "coins_db_cache_bytes": 419430, "coins_tip_cache_bytes": 784649420, "snapshot_blockhash": "00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054", "validated": false } ] } ```
Steps to reproduce the core dump error and its output: 1. Perform a `loadtxoutset` in `mainnet` on compiled `bitcoind` adding the block hash from Sjors's [commit](https://github.com/Sjors/bitcoin/commit/24deb2022b822f22fba9fcbee201e37a83225eb2). 2. Once step 1 finishes, remove the added code from step 1 and compile again or just compile `master` without any changes on top. 3. Run `bitcoind`, soon it'll crash with: ``` 2023-10-18T17:42:52Z [init] init message: Loading block index… 2023-10-18T17:42:52Z [init] Assuming ancestors of block 00000000000000000001a0a448d6cf2546b06801389cc030b2b18c6491266815 have valid signatures. 2023-10-18T17:42:52Z [init] Setting nMinimumChainWork=000000000000000000000000000000000000000052b2559353df4117b7348b64 2023-10-18T17:42:52Z [init] Prune configured to target 3000 MiB on disk for block and undo files. 2023-10-18T17:42:52Z [init] [snapshot] detected active snapshot chainstate (/tmp/.test_utxo_2/chainstate_snapshot) - loading 2023-10-18T17:42:52Z [init] [snapshot] switching active chainstate to Chainstate [snapshot] @ height -1 (null) 2023-10-18T17:42:52Z [init] Opening LevelDB in /tmp/.test_utxo_2/blocks/index 2023-10-18T17:42:52Z [init] Opened LevelDB successfully 2023-10-18T17:42:52Z [init] Using obfuscation key for /tmp/.test_utxo_2/blocks/index: 0000000000000000 node/blockstorage.cpp:390 LoadBlockIndex: Assertion `GetParams().AssumeutxoForBlockhash(*snapshot_blockhash)' failed. Aborted (core dumped) ```
After original change, error message output: ``` 2023-10-20T15:49:12Z [init] init message: Loading block index… 2023-10-20T15:49:12Z [init] Assuming ancestors of block 00000000000000000001a0a448d6cf2546b06801389cc030b2b18c6491266815 have valid signatures. 2023-10-20T15:49:12Z [init] Setting nMinimumChainWork=000000000000000000000000000000000000000052b2559353df4117b7348b64 2023-10-20T15:49:12Z [init] Prune configured to target 3000 MiB on disk for block and undo files. 2023-10-20T15:49:12Z [init] [snapshot] detected active snapshot chainstate (/tmp/.test_utxo_2/chainstate_snapshot) - loading 2023-10-20T15:49:12Z [init] [snapshot] switching active chainstate to Chainstate [snapshot] @ height -1 (null) 2023-10-20T15:49:12Z [init] Opening LevelDB in /tmp/.test_utxo_2/blocks/index 2023-10-20T15:49:12Z [init] Opened LevelDB successfully 2023-10-20T15:49:12Z [init] Using obfuscation key for /tmp/.test_utxo_2/blocks/index: 0000000000000000 2023-10-20T15:49:13Z [init] *** Assumeutxo data not found for the given blockhash '00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054'. 2023-10-20T15:49:13Z [init] Error: Assumeutxo data not found for the given blockhash '00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054'. Error: Assumeutxo data not found for the given blockhash '00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054'. 2023-10-20T15:49:13Z [init] Shutdown requested. Exiting. 2023-10-20T15:49:13Z [init] Shutdown: In progress... 2023-10-20T15:49:13Z [scheduler] scheduler thread exit 2023-10-20T15:49:13Z [shutoff] Flushed fee estimates to fee_estimates.dat. 2023-10-20T15:49:13Z [shutoff] Shutdown: done ```
Alternative on error handling using return error() instead of return FatalError() used in this PR, which produces a different output and perhaps confusing: ``` 2023-10-20T21:45:58Z [init] Using obfuscation key for /tmp/.test_utxo_2/blocks/index: 0000000000000000 2023-10-20T21:45:59Z [init] ERROR: Assumeutxo data not found for the given blockhash '00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054'. 2023-10-20T21:45:59Z [init] : Error loading block database. Please restart with -reindex or -reindex-chainstate to recover. : Error loading block database. Please restart with -reindex or -reindex-chainstate to recover. 2023-10-20T21:45:59Z [init] Aborted block database rebuild. Exiting. 2023-10-20T21:45:59Z [init] Shutdown: In progress... 2023-10-20T21:45:59Z [scheduler] scheduler thread exit 2023-10-20T21:45:59Z [shutoff] Flushed fee estimates to fee_estimates.dat. 2023-10-20T21:45:59Z [shutoff] Shutdown: done ```
Current state (including ryanofsky suggestion), after code change, error message output: ``` 2023-10-25T02:29:57Z [init] Using obfuscation key for /home/pablo/.test_utxo_2/regtest/blocks/index: 0000000000000000 2023-10-25T02:29:57Z [init] *** Assumeutxo data not found for the given blockhash 'f09b5835f3f8b39481f2af3257bbc2e82845552d4d2d6d31cf520fc24263ed5b'. 2023-10-25T02:29:57Z [init] Error: A fatal internal error occurred, see debug.log for details Error: A fatal internal error occurred, see debug.log for details 2023-10-25T02:29:57Z [init] Shutdown requested. Exiting. 2023-10-25T02:29:57Z [init] Shutdown: In progress... 2023-10-25T02:29:57Z [scheduler] scheduler thread exit 2023-10-25T02:29:57Z [shutoff] Flushed fee estimates to fee_estimates.dat. 2023-10-25T02:29:57Z [shutoff] Shutdown: done ```
ACKs for top commit: naumenkogs: ACK 811067ca1cbbd4a697791cbe3ecd4bee19fe6193 theStack: ACK 811067ca1cbbd4a697791cbe3ecd4bee19fe6193 ryanofsky: Code review ACK 811067ca1cbbd4a697791cbe3ecd4bee19fe6193. Tree-SHA512: cfc137b0a4f638b99fd7dac2c35cc729ef71ae1166a2a8960a91055ec90841cb33aed589834012cfe0e157937e2a76a88d1020ea1df2bc98e1114eb1fc8eaae4 --- src/node/blockstorage.cpp | 7 ++++++- test/functional/feature_assumeutxo.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index ba857b3aa848..6b710e1784eb 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -319,7 +319,12 @@ bool BlockManager::LoadBlockIndex(const std::optional& snapshot_blockha } if (snapshot_blockhash) { - const AssumeutxoData au_data = *Assert(GetParams().AssumeutxoForBlockhash(*snapshot_blockhash)); + const std::optional maybe_au_data = GetParams().AssumeutxoForBlockhash(*snapshot_blockhash); + if (!maybe_au_data) { + AbortNode(strprintf("Assumeutxo data not found for the given blockhash '%s'.", snapshot_blockhash->ToString())); + return false; + } + const AssumeutxoData& au_data = *Assert(maybe_au_data); m_snapshot_height = au_data.height; CBlockIndex* base{LookupBlockIndex(*snapshot_blockhash)}; diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 48531c65718c..4bd5a3357167 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -108,6 +108,25 @@ def expected_error(log_msg="", rpc_details=""): f.write(valid_snapshot_contents[(32 + 8 + offset + len(content)):]) expected_error(log_msg=f"[snapshot] bad snapshot content hash: expected d7f46f9830ea11f1bfc565b08f63b66f09e1403b54c988ede40461cf0846fcba, got {wrong_hash}") + def test_invalid_chainstate_scenarios(self, node_index): + self.log.info("Test different scenarios of invalid snapshot chainstate in datadir") + + self.log.info(" - snapshot chainstate refering to a block that is not in the assumeutxo parameters") + node = self.nodes[node_index] + self.stop_node(node_index) + base_blockhash_path = node.chain_path / "chainstate_snapshot" / "base_blockhash" + valid_base_blockhash = base_blockhash_path.read_bytes() + with open(base_blockhash_path, 'wb') as f: + f.write(b'z' * 32) + expected_error = f"Error: A fatal internal error occurred, see debug.log for details" + node.assert_start_raises_init_error(expected_msg=expected_error) + + # Restore the valid base hash and resume the real Dash snapshot. Using + # an activated snapshot preserves the required EvoDB SNAPSHOT marker, + # so the corrupt-hash startup reaches BlockManager::LoadBlockIndex. + base_blockhash_path.write_bytes(valid_base_blockhash) + self.start_node(node_index, extra_args=self.extra_args[node_index]) + def run_test(self): """ Bring up two (disconnected) nodes, mine some new blocks on the first, @@ -263,6 +282,8 @@ def check_for_final_height(): assert_equal(snapshot['snapshot_blockhash'], dump_output['base_hash']) assert_equal(snapshot['validated'], False) + self.test_invalid_chainstate_scenarios(2) + self.connect_nodes(0, 2) self.wait_until(lambda: n2.getchainstates()['chainstates'][-1]['blocks'] == FINAL_HEIGHT) self.sync_blocks() From dfbf0a172857e3165f0d182e1e071bbd5ace7b57 Mon Sep 17 00:00:00 2001 From: fanquake Date: Fri, 10 Nov 2023 09:41:25 +0000 Subject: [PATCH 16/30] Merge bitcoin/bitcoin#28835: test: Check error details with assert_debug_log on the assumeutxo invalid hash dump - follow-up #28698 7de76853728b423339d17f39224cf20305da1832 test, assumeutxo: Use assert_debug_log for error details (pablomartin4btc) Pull request description: This is a follow-up on the invalid hash dump fix #28698, [suggested](https://github.com/bitcoin/bitcoin/pull/28698#pullrequestreview-1698178157) by theStack and agreed by Sjors and ryanofsky. ACKs for top commit: Sjors: ACK 7de76853728b423339d17f39224cf20305da1832 maflcko: lgtm ACK 7de76853728b423339d17f39224cf20305da1832 Tree-SHA512: 036b3cef3084e3ead8923e8dcabe4fa7ebe97fb514d223aa38bc38df10337e3fe3113e42322178b58fb03fcd4511af4b5b56bceecbb7ded5b9758842c70db3f2 --- test/functional/feature_assumeutxo.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 4bd5a3357167..f2f4ff83ef35 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -118,8 +118,14 @@ def test_invalid_chainstate_scenarios(self, node_index): valid_base_blockhash = base_blockhash_path.read_bytes() with open(base_blockhash_path, 'wb') as f: f.write(b'z' * 32) - expected_error = f"Error: A fatal internal error occurred, see debug.log for details" - node.assert_start_raises_init_error(expected_msg=expected_error) + + def expected_error(log_msg="", error_msg=""): + with node.assert_debug_log([log_msg]): + node.assert_start_raises_init_error(expected_msg=error_msg) + + expected_error_msg = f"Error: A fatal internal error occurred, see debug.log for details" + error_details = f"Assumeutxo data not found for the given blockhash" + expected_error(log_msg=error_details, error_msg=expected_error_msg) # Restore the valid base hash and resume the real Dash snapshot. Using # an activated snapshot preserves the required EvoDB SNAPSHOT marker, From cda7872b094b9572c480ba03a3b68dec07df5742 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 11 Jan 2024 11:23:55 -0500 Subject: [PATCH 17/30] Merge bitcoin/bitcoin#28838: test: add assumeutxo wallet test 997b9a73e5166b4244f7c5b4fe144d524f3005f4 test: add assumeutxo wallet test (Sjors Provoost) Pull request description: Extracted from #28616, this adds a (very) basic wallet test for assume utxo. It checks some circumstances where a backup can and can't be loaded. ACKs for top commit: maflcko: lgtm ACK 997b9a73e5166b4244f7c5b4fe144d524f3005f4 achow101: ACK 997b9a73e5166b4244f7c5b4fe144d524f3005f4 theStack: Code-review ACK 997b9a73e5166b4244f7c5b4fe144d524f3005f4 Tree-SHA512: 69474e56c6a46bb4f30fc54f8e5844766ac2a5f8226bb0b168d11ae1e3d4eae58570c1f1b4cc2b2f6f51b5d0e055bbe2bbd11684265215e01d4eb81ab4b7b0bb --- test/functional/test_runner.py | 1 + test/functional/wallet_assumeutxo.py | 165 +++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100755 test/functional/wallet_assumeutxo.py diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 6b8e1095c187..7413b7aa2ec8 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -380,6 +380,7 @@ 'feature_filelock.py', 'feature_loadblock.py', 'feature_assumeutxo.py', + 'wallet_assumeutxo.py --descriptors', 'p2p_dos_header_tree.py', 'p2p_add_connections.py', 'feature_bind_port_discover.py', diff --git a/test/functional/wallet_assumeutxo.py b/test/functional/wallet_assumeutxo.py new file mode 100755 index 000000000000..74140c9c4a12 --- /dev/null +++ b/test/functional/wallet_assumeutxo.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test for assumeutxo wallet related behavior. +See feature_assumeutxo.py for background. + +## Possible test improvements + +- TODO: test import descriptors while background sync is in progress +- TODO: test loading a wallet (backup) on a pruned node + +""" +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) + +START_HEIGHT = 199 +SNAPSHOT_BASE_HEIGHT = 299 +FINAL_HEIGHT = 399 + + +class AssumeutxoTest(BitcoinTestFramework): + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def add_options(self, parser): + self.add_wallet_options(parser, legacy=False) + + def set_test_params(self): + """Use the pregenerated, deterministic chain up to height 199.""" + self.num_nodes = 2 + self.rpc_timeout = 120 + self.extra_args = [ + [], + [], + ] + + def setup_network(self): + """Start with the nodes disconnected so that one can generate a snapshot + including blocks the other hasn't yet seen.""" + self.add_nodes(2) + self.start_nodes(extra_args=self.extra_args) + + def run_test(self): + """ + Bring up two (disconnected) nodes, mine some new blocks on the first, + and generate a UTXO snapshot. + + Load the snapshot into the second, ensure it syncs to tip and completes + background validation when connected to the first. + """ + n0 = self.nodes[0] + n1 = self.nodes[1] + + # Mock time for a deterministic chain + for n in self.nodes: + n.setmocktime(n.getblockheader(n.getbestblockhash())['time']) + + self.sync_blocks() + + n0.createwallet('w') + w = n0.get_wallet_rpc("w") + + # Generate a series of blocks that `n0` will have in the snapshot, + # but that n1 doesn't yet see. In order for the snapshot to activate, + # though, we have to ferry over the new headers to n1 so that it + # isn't waiting forever to see the header of the snapshot's base block + # while disconnected from n0. + for _ in range(100): + self.generate(n0, nblocks=1, sync_fun=self.no_op) + newblock = n0.getblock(n0.getbestblockhash(), 0) + + # make n1 aware of the new header, but don't give it the block. + n1.submitheader(newblock) + + # Ensure everyone is seeing the same headers. + for n in self.nodes: + assert_equal(n.getblockchaininfo()[ + "headers"], SNAPSHOT_BASE_HEIGHT) + + w.backupwallet("backup_w.dat") + + self.log.info("-- Testing assumeutxo") + + assert_equal(n0.getblockcount(), SNAPSHOT_BASE_HEIGHT) + assert_equal(n1.getblockcount(), START_HEIGHT) + + self.log.info( + f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}") + dump_output = n0.dumptxoutset('utxos.dat') + + assert_equal( + dump_output['txoutset_hash'], + 'd7f46f9830ea11f1bfc565b08f63b66f09e1403b54c988ede40461cf0846fcba') + assert_equal(dump_output['evo_hash'], 'f2ccd3fef604df58a0c174489821e16912c9332969a267650cd040e85fb2adde') + assert_equal(dump_output['nchaintx'], 300) + assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) + + # Mine more blocks on top of the snapshot that n1 hasn't yet seen. This + # will allow us to test n1's sync-to-tip on top of a snapshot. + self.generate(n0, nblocks=100, sync_fun=self.no_op) + + assert_equal(n0.getblockcount(), FINAL_HEIGHT) + assert_equal(n1.getblockcount(), START_HEIGHT) + + assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT) + + self.log.info( + f"Loading snapshot into second node from {dump_output['path']}") + loaded = n1.loadtxoutset(dump_output['path']) + assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) + assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT) + + normal, snapshot = n1.getchainstates()["chainstates"] + assert_equal(normal['blocks'], START_HEIGHT) + assert_equal(normal.get('snapshot_blockhash'), None) + assert_equal(normal['validated'], True) + assert_equal(snapshot['blocks'], SNAPSHOT_BASE_HEIGHT) + assert_equal(snapshot['snapshot_blockhash'], dump_output['base_hash']) + assert_equal(snapshot['validated'], False) + + assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) + + self.log.info("Backup can't be loaded during background sync") + assert_raises_rpc_error(-4, "Wallet loading failed. Error loading wallet. Wallet requires blocks to be downloaded, and software does not currently support loading wallets while blocks are being downloaded out of order when using assumeutxo snapshots. Wallet should be able to load successfully after node sync reaches height 299", n1.restorewallet, "w", "backup_w.dat") + + PAUSE_HEIGHT = FINAL_HEIGHT - 40 + + self.log.info("Restarting node to stop at height %d", PAUSE_HEIGHT) + self.restart_node(1, extra_args=[ + f"-stopatheight={PAUSE_HEIGHT}", *self.extra_args[1]]) + + # Finally connect the nodes and let them sync. + # + # Set `wait_for_connect=False` to avoid a race between performing connection + # assertions and the -stopatheight tripping. + self.connect_nodes(0, 1, wait_for_connect=False) + + n1.wait_until_stopped(timeout=5) + + self.log.info( + "Restarted node before snapshot validation completed, reloading...") + self.restart_node(1, extra_args=self.extra_args[1]) + + # TODO: inspect state of e.g. the wallet before reconnecting + self.connect_nodes(0, 1) + + self.log.info( + f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})") + self.wait_until(lambda: n1.getchainstates()[ + 'chainstates'][-1]['blocks'] == FINAL_HEIGHT) + self.sync_blocks(nodes=(n0, n1)) + + self.log.info("Ensuring background validation completes") + self.wait_until(lambda: len(n1.getchainstates()['chainstates']) == 1) + + self.log.info("Ensuring wallet can be restored from backup") + n1.restorewallet("w", "backup_w.dat") + + +if __name__ == '__main__': + AssumeutxoTest().main() From 3917b0323795568e9e6051d4a80b64a893e88aca Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 16:41:37 -0500 Subject: [PATCH 18/30] feat: allow exact regtest AssumeUTXO authorization --- src/chainparams.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index f3b7682a2002..9e04f8fa1d79 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -907,6 +907,34 @@ class CRegTestParams : public CChainParams { }, }; + for (const std::string& arg : args.GetArgs("-assumeutxodata")) { + const std::vector fields{SplitString(arg, ':')}; + int32_t height; + uint32_t n_chain_tx; + const auto valid_hash = [](const std::string& value) { + return value.size() == uint256::size() * 2 && IsHex(value) && !uint256S(value).IsNull(); + }; + if (fields.size() != 5 || !ParseInt32(fields[0], &height) || height <= 0 || + !valid_hash(fields[1]) || !valid_hash(fields[2]) || + !ParseUInt32(fields[3], &n_chain_tx) || n_chain_tx == 0 || !valid_hash(fields[4])) { + throw std::runtime_error(strprintf( + "Invalid value (%s) for -assumeutxodata=::::.", + arg)); + } + const uint256 blockhash{uint256S(fields[4])}; + if (AssumeutxoForHeight(height) || AssumeutxoForBlockhash(blockhash)) { + throw std::runtime_error(strprintf( + "Duplicate height or block hash in -assumeutxodata (%s).", arg)); + } + m_assumeutxo_data.emplace_back(AssumeutxoData{ + .height = height, + .hash_serialized = AssumeutxoHash{uint256S(fields[1])}, + .evo_hash = EvoSnapshotHash{uint256S(fields[2])}, + .nChainTx = n_chain_tx, + .blockhash = blockhash, + }); + } + chainTxData = ChainTxData{ 0, 0, @@ -1409,6 +1437,7 @@ void SetupChainParamsOptions(ArgsManager& argsman) SetupChainParamsBaseOptions(argsman); argsman.AddArg("-budgetparams=::", "Override masternode, budget and superblock start heights (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + argsman.AddArg("-assumeutxodata=::::", "Add an exact AssumeUTXO snapshot authorization (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-dip3params=:", "Override DIP3 activation and enforcement heights (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-highsubsidyblocks=", "The number of blocks with a higher than normal subsidy to mine at the start of a chain. Block after that height will have fixed subsidy base. (default: 0, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-highsubsidyfactor=", "The factor to multiply the normal block subsidy by while in the highsubsidyblocks window of a chain (default: 1, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); From f84c3b647ddd96e4c7b670605848371d6f65c1b6 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 16:41:38 -0500 Subject: [PATCH 19/30] fix: preserve assumed-valid block index state --- src/validation.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index a027c95fa7c7..cc4c7451e202 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3937,8 +3937,11 @@ void Chainstate::TryAddBlockIndexCandidate(CBlockIndex* pindex) void ChainstateManager::ReceivedBlockTransactions(const CBlock& block, CBlockIndex* pindexNew, const FlatFilePos& pos) { AssertLockHeld(cs_main); + const bool is_snapshot_base{GetSnapshotBaseBlock() == pindexNew}; pindexNew->nTx = block.vtx.size(); - pindexNew->nChainTx = 0; + if (!is_snapshot_base) { + pindexNew->nChainTx = 0; + } pindexNew->nFile = pos.nFile; pindexNew->nDataPos = pos.nPos; pindexNew->nUndoPos = 0; @@ -3946,7 +3949,15 @@ void ChainstateManager::ReceivedBlockTransactions(const CBlock& block, CBlockInd pindexNew->RaiseValidity(BLOCK_VALID_TRANSACTIONS); m_blockman.m_dirty_blockindex.insert(pindexNew); - if (pindexNew->pprev == nullptr || pindexNew->pprev->HaveNumChainTxs()) { + if (is_snapshot_base) { + // The authorized cumulative count was seeded when the snapshot was + // activated. Preserve it if the base block arrives before its history. + Assert(pindexNew->nChainTx > 0); + pindexNew->nSequenceId = nBlockSequenceId++; + for (Chainstate* chainstate : GetAll()) { + chainstate->TryAddBlockIndexCandidate(pindexNew); + } + } else if (pindexNew->pprev == nullptr || pindexNew->pprev->HaveNumChainTxs()) { // If pindexNew is the genesis block or all parents are BLOCK_VALID_TRANSACTIONS. std::deque queue; queue.push_back(pindexNew); @@ -5274,7 +5285,10 @@ void ChainstateManager::CheckBlockIndex() // For testing, allow transaction counts to be completely unset. || (pindex->nChainTx == 0 && pindex->nTx == 0) // For testing, allow this nChainTx to be unset if previous is also unset. - || (pindex->nChainTx == 0 && prev_chain_tx == 0 && pindex->pprev)); + || (pindex->nChainTx == 0 && prev_chain_tx == 0 && pindex->pprev) + // Snapshot entries can have seeded or partially linked counts + // until background validation removes BLOCK_ASSUMED_VALID. + || pindex->IsAssumedValid()); if (pindexFirstAssumeValid == nullptr && pindex->nStatus & BLOCK_ASSUMED_VALID) pindexFirstAssumeValid = pindex; if (pindexFirstInvalid == nullptr && pindex->nStatus & BLOCK_FAILED_VALID) pindexFirstInvalid = pindex; if (pindexFirstConflicing == nullptr && pindex->nStatus & BLOCK_CONFLICT_CHAINLOCK) pindexFirstConflicing = pindex; From aecec1aea7478a5c76b5d2830921887f8882e69e Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 16:41:39 -0500 Subject: [PATCH 20/30] fix: keep snapshot quorum lookups cache-only --- src/llmq/snapshot.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/llmq/snapshot.cpp b/src/llmq/snapshot.cpp index 661777127c5a..23dc5494b44f 100644 --- a/src/llmq/snapshot.cpp +++ b/src/llmq/snapshot.cpp @@ -317,6 +317,18 @@ void CQuorumSnapshotManager::StoreSnapshotForBlock(const Consensus::LLMQType llm { auto snapshotHash = ::SerializeHash(std::make_pair(llmqType, pindex->GetBlockHash())); + // Snapshot activation seeds the historical rotation snapshots needed to + // reconstruct active quorums. RPC/P2P lookups can additionally derive the + // current cycle while no block transaction is active. Keep that result in + // memory: writing it here would dirty the default EvoDB transaction and + // make a later flush/shutdown fail. Once dual-chainstate mode ends, normal + // block processing remains responsible for durable derived snapshots. + if (!m_evoDb.HasActiveTransaction() && m_evoDb.HasDualChainstateMarker()) { + LOCK(snapshotCacheCs); + quorumSnapshotCache.insert(snapshotHash, snapshot); + return; + } + if (!m_evoDb.WriteDerived(std::make_pair(DB_QUORUM_SNAPSHOT, snapshotHash), snapshot)) { throw std::runtime_error("EvoDB quorum snapshot payload mismatch"); } From 7332dc1fefa2f9f5efadcdbc32775cd4ddfdc63b Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 16:41:40 -0500 Subject: [PATCH 21/30] fix: initialize BLS before snapshot cleanup --- src/node/chainstate.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp index e155742206f8..815cf971b881 100644 --- a/src/node/chainstate.cpp +++ b/src/node/chainstate.cpp @@ -356,6 +356,14 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize return {init_status, init_error}; } + // Snapshot completion can inspect v19+ CbTx BLS signatures before + // VerifyLoadedChainstate() performs its usual scheme update. + if (const CBlockIndex* tip{chainman.ActiveChainstate().m_chain.Tip()}; + tip && DeploymentActiveAfter(tip, chainman, Consensus::DEPLOYMENT_V19)) { + bls::bls_legacy_scheme.store(false); + LogPrintf("LoadChainstate: bls_legacy_scheme=%d\n", bls::bls_legacy_scheme.load()); + } + // If a snapshot chainstate was fully validated by a background chainstate during // the last run, detect it here and clean up the now-unneeded background // chainstate. From 360e9765f3ccc79b3e7630d63bf2ddab3363b245 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 16:41:41 -0500 Subject: [PATCH 22/30] test: expand feature_assumeutxo_dash.py to full E2E --- test/functional/feature_assumeutxo_dash.py | 388 +++++++++++++++++++-- 1 file changed, 363 insertions(+), 25 deletions(-) diff --git a/test/functional/feature_assumeutxo_dash.py b/test/functional/feature_assumeutxo_dash.py index ed5c43509bba..965581ee02b0 100755 --- a/test/functional/feature_assumeutxo_dash.py +++ b/test/functional/feature_assumeutxo_dash.py @@ -3,24 +3,62 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Exercise Dash evo emission by dumptxoutset (loading is added in M5).""" +"""End-to-end AssumeUTXO coverage for Dash deterministic state. +Build a DIP3/v20 chain with real masternodes and both rotated and non-rotated +quorums, load its dynamically authorized snapshot, and exercise validation, +restart recovery, LLMQ use, and malformed evo sections. +""" + +import subprocess +from io import BytesIO from pathlib import Path +from test_framework.messages import msg_isdlock +from test_framework.p2p import P2PInterface from test_framework.test_framework import DashTestFramework -from test_framework.util import assert_equal +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + force_finish_mnsync, + initialize_datadir, +) + + +LLMQ_TEST = 100 +LLMQ_TEST_DIP0024 = 103 +EVO_MARKER = b"DASHEVO\x00" +REGTEST_SPORK_KEY = "cP4EKFyJsHT39LDqgdcB43Y3YXjNyjb5Fuas1GQSeAtjnZWmZEQK" +COMPILED_HEIGHT = 110 +COMPILED_BLOCK_HASH = "729bcb1479ff9f4968439f0276bd76bcb2de0f0720b7a16f383321f6a41cb238" + +# These are local views, not deterministic masternode-list state: +# - confirmations depends on the node's locally active UTXO chainstate; +# - wallet describes locally held keys/scripts (and is absent with -disablewallet); +# - metaInfo is locally learned connection/mixing metadata. +LOCAL_PROTX_FIELDS = {"confirmations", "wallet", "metaInfo"} +QUORUM_COMMITMENT_FIELDS = ( + "height", + "type", + "quorumHash", + "quorumIndex", + "minedBlock", + "previousConsecutiveDKGFailures", + "quorumPublicKey", +) +QUORUM_MEMBER_FIELDS = ("proTxHash", "pubKeyOperator", "valid", "pubKeyShare") class AssumeutxoDashTest(DashTestFramework): def set_test_params(self): - # Keep rotation out of this minimal M4 emission test. The three enabled - # non-rotated test types each need two active plus one safety quorum. - args = [[ - "-testactivationheight=dip0024@999999", - "-vbparams=testdummy:999999999999:999999999999", - ] for _ in range(4)] - self.set_dash_test_params(4, 3, extra_args=args, evo_count=3) + # Rotation produces two four-member indexed quorums per cycle, so the + # fixture needs eight masternodes. Delay v20 to follow the established + # rotation fixture: first make ordinary quorums, then activate rotation. + args = [["-vbparams=testdummy:999999999999:999999999999"] for _ in range(6)] + self.set_dash_test_params(6, 5, extra_args=args, evo_count=3) self.set_dash_llmq_test_params(3, 2) + self.delay_v20_and_mn_rr(height=300) + self.rpc_timeout = 180 def add_options(self, parser): self.add_wallet_options(parser) @@ -28,28 +66,328 @@ def add_options(self, parser): def skip_test_if_missing_module(self): self.skip_if_no_wallet() + def add_snapshot_node(self, assumeutxo_arg): + node_index = len(self.nodes) + extra_args = [ + *self.extra_args[0], + assumeutxo_arg, + "-disablewallet", + f"-sporkkey={REGTEST_SPORK_KEY}", + ] + initialize_datadir(self.options.tmpdir, node_index, self.chain, self.disable_autoconnect) + self.add_nodes(1, extra_args=[extra_args]) + self.start_node(node_index, extra_args) + return node_index, extra_args + + def submit_headers(self, node, source, height, start_height=1): + for block_height in range(start_height, height + 1): + node.submitheader(source.getblock(source.getblockhash(block_height), 0)) + assert_equal(node.getblockchaininfo()["headers"], height) + + def assert_unvalidated_snapshot(self, node, base_height, base_hash, background_height): + normal, snapshot = node.getchainstates()["chainstates"] + assert_equal(normal["blocks"], background_height) + assert_equal(normal["validated"], True) + assert "snapshot_blockhash" not in normal + assert_equal(snapshot["blocks"], base_height) + assert_equal(snapshot["snapshot_blockhash"], base_hash) + assert_equal(snapshot["validated"], False) + + def normalized_protx_state(self, node, height): + entries = node.protx("list", "registered", True, height) + return sorted( + ({key: value for key, value in entry.items() if key not in LOCAL_PROTX_FIELDS} + for entry in entries), + key=lambda entry: entry["proTxHash"], + ) + + def normalized_quorum_info(self, node, llmq_type, quorum_hash): + info = node.quorum("info", llmq_type, quorum_hash) + return { + "commitment": {field: info[field] for field in QUORUM_COMMITMENT_FIELDS if field in info}, + # Preserve RPC order: member position is commitment-significant. + "members": [ + {field: member[field] for field in QUORUM_MEMBER_FIELDS if field in member} + for member in info["members"] + ], + } + + def test_assumeutxodata_startup(self, node, valid_arg): + self.log.info("Reject malformed and colliding -assumeutxodata entries on regtest") + fields = valid_arg.removeprefix("-assumeutxodata=").split(":") + valid_hash = fields[1] + invalid_values = [ + ":".join(fields[:-1]), # too few fields + ":".join([*fields, "extra"]), # too many fields + ":".join(["0", *fields[1:]]), # height out of range + ":".join(["2147483648", *fields[1:]]), # height over int32 + ":".join([fields[0], *fields[1:3], "4294967296", fields[4]]), # nchaintx over uint32 + ":".join([fields[0], "0" * 64, *fields[2:]]), # null serialized hash + ":".join([fields[0], "not-a-hash", *fields[2:]]), # malformed serialized hash + ":".join([*fields[:2], "0" * 64, *fields[3:]]), # null evo hash + ":".join([*fields[:2], "not-a-hash", *fields[3:]]), # malformed evo hash + ":".join([*fields[:4], "0" * 64]), # null block hash + ":".join([*fields[:4], "not-a-hash"]), # malformed block hash + ] + for value in invalid_values: + node.assert_start_raises_init_error( + extra_args=[f"-assumeutxodata={value}"], + expected_msg=( + f"Error: Invalid value ({value}) for -assumeutxodata=::" + "::." + ), + ) + + unique_a = f"901:{valid_hash}:{fields[2]}:902:{'01'.zfill(64)}" + duplicate_height = f"901:{valid_hash}:{fields[2]}:903:{'02'.zfill(64)}" + duplicate_hash = f"902:{valid_hash}:{fields[2]}:903:{'01'.zfill(64)}" + compiled_height = f"{COMPILED_HEIGHT}:{valid_hash}:{fields[2]}:903:{'03'.zfill(64)}" + compiled_hash = f"903:{valid_hash}:{fields[2]}:904:{COMPILED_BLOCK_HASH}" + for args, duplicate in ( + ([unique_a, duplicate_height], duplicate_height), + ([unique_a, duplicate_hash], duplicate_hash), + ([compiled_height], compiled_height), + ([compiled_hash], compiled_hash), + ): + node.assert_start_raises_init_error( + extra_args=[f"-assumeutxodata={value}" for value in args], + expected_msg=f"Error: Duplicate height or block hash in -assumeutxodata ({duplicate}).", + ) + + self.log.info("Ignore -assumeutxodata silently on a non-regtest startup") + node.start( + extra_args=["-regtest=0", "-testnet", "-server=0", "-listen=0", "-dnsseed=0", "-assumeutxodata=malformed"], + ) + try: + try: + node.process.wait(timeout=2) + except subprocess.TimeoutExpired: + pass + else: + raise AssertionError("non-regtest node rejected an ignored -assumeutxodata value") + finally: + node.process.terminate() + node.process.wait(timeout=10) + node.running = False + node.process = None + + def relay_islock(self, node, raw_tx, islock_hex, txid): + # Sporks are network state, not part of the snapshot. Enable IS locally + # without connecting this isolated node and starting block download. + node.sporkupdate("SPORK_2_INSTANTSEND_ENABLED", 0) + force_finish_mnsync(node) + assert_equal(node.sendrawtransaction(raw_tx), txid) + islock = msg_isdlock() + islock.deserialize(BytesIO(bytes.fromhex(islock_hex))) + peer = node.add_p2p_connection(P2PInterface()) + peer.send_message(islock) + self.wait_until(lambda: node.getislocks([txid])[0] != "None") + assert_equal(node.getislocks([txid])[0]["hex"], islock_hex) + def run_test(self): - self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0) + node0 = self.nodes[0] + node0.sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0) self.wait_for_sporks_same() + + self.log.info("Add three EvoNodes for llmq_test_platform") for _ in range(self.evo_count): self.dynamically_add_masternode(evo=True) - # Each DKG cycle forms all enabled non-rotated test quorum types. Four - # cycles cover llmq_test_platform's larger safety retention horizon. + self.log.info("Create non-rotated quorums before activating v20") + self.mine_quorum() + self.mine_quorum() + self.activate_v20(expected_activation_height=300) + self.generate(node0, 1) + + self.log.info("Create rotated quorums and fill all active/safety horizons") for _ in range(4): - self.mine_quorum(llmq_type_name="llmq_test", llmq_type=100) - - node = self.nodes[0] - info = node.getblockchaininfo() - assert info["blocks"] >= 100 # DIP3, v19 and v20 are active in DashTestFramework. - result = node.dumptxoutset("assumeutxo-dash.dat") - assert_equal(result["base_height"], node.getblockcount()) - assert len(result["evo_hash"]) == 64 - assert result["evo_mn_count"] >= 3 - - snapshot_path = Path(node.datadir) / self.chain / "assumeutxo-dash.dat" - data = snapshot_path.read_bytes() - assert b"DASHEVO\x00" in data + self.mine_cycle_quorum() + + # The helper finishes after the DKG mining window plus the signing + # maturity offset. This is deliberately outside active DKG phases. + base_height = node0.getblockcount() + assert 10 < base_height % 24 < 24 + base_hash = node0.getbestblockhash() + self.wait_for_chainlocked_block_all_nodes(base_hash, timeout=30) + + deployment_info = node0.getdeploymentinfo() + for deployment in ("dip0003", "dip0008", "dip0024", "v19", "v20"): + assert deployment_info["deployments"][deployment]["active"] + ordinary_quorums = node0.quorum("list", LLMQ_TEST)["llmq_test"] + rotated_quorums = node0.quorum("list", LLMQ_TEST_DIP0024)["llmq_test_dip0024"] + assert ordinary_quorums + assert rotated_quorums + + self.log.info("Keep the M4 dump-side checks and capture both commitments") + dump = node0.dumptxoutset("assumeutxo-dash.dat") + assert_equal(dump["base_height"], base_height) + assert_equal(dump["base_hash"], base_hash) + assert len(dump["txoutset_hash"]) == 64 + assert len(dump["evo_hash"]) == 64 + assert dump["evo_mn_count"] >= 8 + snapshot_path = Path(dump["path"]) + snapshot_bytes = snapshot_path.read_bytes() + marker_offset = snapshot_bytes.index(EVO_MARKER) + assert marker_offset > 40 + + assumeutxo_arg = ( + f"-assumeutxodata={base_height}:{dump['txoutset_hash']}:" + f"{dump['evo_hash']}:{dump['nchaintx']}:{base_hash}" + ) + + self.stop_node(5) + self.test_assumeutxodata_startup(self.nodes[5], assumeutxo_arg) + self.start_node(5) + + self.log.info("An active masternode must refuse snapshot loading") + assert_raises_rpc_error( + -1, + "loadtxoutset is unavailable in masternode mode", + self.mninfo[0].get_node(self).loadtxoutset, + str(snapshot_path), + ) + + self.log.info("Negative evo-section checks") + negative_index, _ = self.add_snapshot_node(assumeutxo_arg) + negative = self.nodes[negative_index] + self.submit_headers(negative, node0, base_height) + + utxo_only_path = snapshot_path.with_suffix(".utxo-only.dat") + utxo_only_path.write_bytes(snapshot_bytes[:marker_offset]) + with negative.assert_debug_log(["missing evo section at DIP3-active base"]): + assert_raises_rpc_error( + -32603, + "missing evo section at DIP3-active base", + negative.loadtxoutset, + str(utxo_only_path), + ) + + # This fixture has no asset-unlock ranges or MNHF signals, so the last + # 26 bytes are three int64 credit-pool fields followed by two zero + # CompactSize counts. Alter currentLimit, which remains structurally + # valid and is intentionally not a CbTx root, to reach the evo hash check. + tampered = bytearray(snapshot_bytes) + assert_equal(tampered[-2:], b"\x00\x00") + tampered[-18] ^= 1 + tampered_path = snapshot_path.with_suffix(".tampered-evo.dat") + tampered_path.write_bytes(tampered) + with negative.assert_debug_log(["bad evo snapshot hash"]): + assert_raises_rpc_error( + -32603, + "evo snapshot hash mismatch", + negative.loadtxoutset, + str(tampered_path), + ) + self.log.info("Load the full snapshot on a fresh non-masternode") + snapshot_index, snapshot_args = self.add_snapshot_node(assumeutxo_arg) + snapshot_node = self.nodes[snapshot_index] + # Leave a bounded tail for the distinct post-restart background phase. + mid_height = base_height - 48 + for height in range(1, mid_height + 1): + assert snapshot_node.submitblock( + node0.getblock(node0.getblockhash(height), 0)) in (None, "duplicate") + assert_equal(snapshot_node.getblockcount(), mid_height) + self.submit_headers(snapshot_node, node0, base_height, mid_height + 1) + loaded = snapshot_node.loadtxoutset(str(snapshot_path)) + assert_equal(loaded["base_height"], base_height) + assert_equal(loaded["tip_hash"], base_hash) + assert_equal(loaded["coins_loaded"], dump["coins_written"]) + self.assert_unvalidated_snapshot(snapshot_node, base_height, base_hash, mid_height) + + self.log.info("Restart immediately after loading the snapshot") + self.restart_node(snapshot_index, extra_args=snapshot_args) + self.assert_unvalidated_snapshot(snapshot_node, base_height, base_hash, mid_height) + + self.log.info("Advance only the disconnected background chainstate") + advanced_height = base_height - 1 + assert advanced_height > mid_height + for height in range(mid_height + 1, advanced_height + 1): + assert snapshot_node.submitblock( + node0.getblock(node0.getblockhash(height), 0)) in (None, "duplicate") + self.assert_unvalidated_snapshot(snapshot_node, base_height, base_hash, advanced_height) + + self.log.info("Compare deterministic masternode and quorum state at the base") + assert_equal( + self.normalized_protx_state(snapshot_node, base_height), + self.normalized_protx_state(node0, base_height), + ) + for llmq_type, quorum_hashes in ( + (LLMQ_TEST, ordinary_quorums), + (LLMQ_TEST_DIP0024, rotated_quorums), + ): + for quorum_hash in quorum_hashes: + assert_equal( + self.normalized_quorum_info(snapshot_node, llmq_type, quorum_hash), + self.normalized_quorum_info(node0, llmq_type, quorum_hash), + ) + + self.log.info("Verify and relay ChainLock and InstantSend locks pre-completion") + best_cl = node0.getbestchainlock() + assert snapshot_node.verifychainlock( + best_cl["blockhash"], best_cl["signature"], best_cl["height"]) + assert_equal( + snapshot_node.submitchainlock( + best_cl["blockhash"], best_cl["signature"], best_cl["height"]), + best_cl["height"], + ) + assert_equal(snapshot_node.getbestchainlock()["blockhash"], best_cl["blockhash"]) + + raw_tx = self.create_raw_tx(node0, node0, 1, 1, 100) + txid = node0.sendrawtransaction(raw_tx["hex"]) + self.wait_for_instantlock(txid, nodes=[node0]) + islock = node0.getislocks([txid])[0] + assert snapshot_node.verifyislock(islock["id"], txid, islock["signature"], base_height) + self.relay_islock(snapshot_node, raw_tx["hex"], islock["hex"], txid) + + assert_raises_rpc_error( + -32603, + "Only available in masternode mode", + snapshot_node.quorum, + "sign", + LLMQ_TEST, + "01".zfill(64), + "02".zfill(64), + ) + + self.log.info("Restart while background validation is paused halfway") + self.restart_node(snapshot_index, extra_args=snapshot_args) + self.assert_unvalidated_snapshot(snapshot_node, base_height, base_hash, advanced_height) + + self.log.info("Complete background validation and its deferred evo comparison") + with snapshot_node.assert_debug_log( + ["has been fully validated"], + unexpected_msgs=["evo state mismatch", "EVO_STATE_MISMATCH"], + timeout=180, + ): + self.connect_nodes(snapshot_index, 0) + self.wait_until(lambda: len(snapshot_node.getchainstates()["chainstates"]) == 1, timeout=180) + self.disconnect_nodes(snapshot_index, 0) + + completed, = snapshot_node.getchainstates()["chainstates"] + assert_equal(completed["blocks"], base_height) + assert_equal(completed["validated"], True) + assert_equal(completed["snapshot_blockhash"], base_hash) + + self.log.info("Restart after completion to run validated cleanup and b_b4 promotion") + with snapshot_node.assert_debug_log( + ["cleaning up unneeded background chainstate", "moving snapshot chainstate"], + unexpected_msgs=["evo state mismatch", "EVO_STATE_MISMATCH"], + timeout=180, + ): + self.restart_node(snapshot_index, extra_args=snapshot_args) + cleaned, = snapshot_node.getchainstates()["chainstates"] + assert_equal(cleaned["blocks"], base_height) + assert_equal(cleaned["validated"], True) + assert "snapshot_blockhash" not in cleaned + assert not (snapshot_node.chain_path / "chainstate_snapshot").exists() + assert not (snapshot_node.chain_path / "chainstate_todelete").exists() + + self.log.info("Restart once more after cleanup") + self.restart_node(snapshot_index, extra_args=snapshot_args) + final_state, = snapshot_node.getchainstates()["chainstates"] + assert_equal(final_state["blocks"], base_height) + assert_equal(final_state["validated"], True) + assert "snapshot_blockhash" not in final_state if __name__ == "__main__": From c6eb2624642bc9d2df6a2714b1113cd5e46a6175 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 17:12:07 -0500 Subject: [PATCH 23/30] refactor: break blockfilter circular dependency from bitcoin#27596 work --- src/interfaces/chain.h | 16 +--------------- src/kernel/chain.cpp | 1 - src/kernel/chain.h | 16 +++++++++++++++- test/lint/lint-circular-dependencies.py | 1 - 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index 65bcd0df16b1..b83818c1350a 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -6,6 +6,7 @@ #define BITCOIN_INTERFACES_CHAIN_H #include +#include // IWYU pragma: export #include // For CTransactionRef #include // For util::SettingsValue @@ -25,7 +26,6 @@ class CRPCCommand; class CScheduler; class CFeeRate; class CBlockIndex; -class CBlockUndo; class Coin; class uint256; enum class MemPoolRemovalReason; @@ -48,20 +48,6 @@ typedef std::shared_ptr CTransactionRef; namespace interfaces { -//! Block data sent with blockConnected and blockDisconnected notifications. -struct BlockInfo { - const uint256& hash; - const uint256* prev_hash{nullptr}; - int height{-1}; - int file_number{-1}; - unsigned data_pos{0}; - const CBlock* data{nullptr}; - const CBlockUndo* undo_data{nullptr}; - unsigned int chain_time_max{0}; - - explicit BlockInfo(const uint256& block_hash) : hash(block_hash) {} -}; - class Wallet; class Handler; diff --git a/src/kernel/chain.cpp b/src/kernel/chain.cpp index 61cea51bd92b..a248e4dc3f1d 100644 --- a/src/kernel/chain.cpp +++ b/src/kernel/chain.cpp @@ -3,7 +3,6 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include -#include #include #include #include diff --git a/src/kernel/chain.h b/src/kernel/chain.h index d499af333444..4996c799551d 100644 --- a/src/kernel/chain.h +++ b/src/kernel/chain.h @@ -21,8 +21,22 @@ interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* block_index, const CBlock class CBlock; class CBlockIndex; +class CBlockUndo; +class uint256; namespace interfaces { -struct BlockInfo; +//! Block data sent with blockConnected and blockDisconnected notifications. +struct BlockInfo { + const uint256& hash; + const uint256* prev_hash{nullptr}; + int height{-1}; + int file_number{-1}; + unsigned data_pos{0}; + const CBlock* data{nullptr}; + const CBlockUndo* undo_data{nullptr}; + unsigned int chain_time_max{0}; + + explicit BlockInfo(const uint256& block_hash) : hash(block_hash) {} +}; } // namespace interfaces namespace kernel { diff --git a/test/lint/lint-circular-dependencies.py b/test/lint/lint-circular-dependencies.py index bc0a693a42bd..1d0a616933fa 100755 --- a/test/lint/lint-circular-dependencies.py +++ b/test/lint/lint-circular-dependencies.py @@ -33,7 +33,6 @@ "index/base -> node/context -> index/spentindex -> index/base", "index/base -> node/context -> index/timestampindex -> index/base", "banman -> common/bloom -> evo/assetlocktx -> llmq/quorumsman -> llmq/blockprocessor -> net -> banman", - "blockfilter -> evo/specialtx_filter -> evo/providertx -> validation -> kernel/chain -> interfaces/chain.h -> blockfilter", "coinjoin/client -> coinjoin/util -> wallet/wallet -> psbt -> node/transaction -> net_processing -> coinjoin/walletman -> coinjoin/client", "common/bloom -> evo/assetlocktx -> llmq/commitment -> evo/deterministicmns -> evo/simplifiedmns -> merkleblock -> common/bloom", "common/bloom -> evo/assetlocktx -> llmq/quorumsman -> llmq/blockprocessor -> net -> common/bloom", From e08d111ff5e02c65d987af5955ca782e0efead20 Mon Sep 17 00:00:00 2001 From: Pasta Date: Tue, 14 Jul 2026 11:20:40 -0500 Subject: [PATCH 24/30] test: avoid restoring removed global txindex --- src/test/validation_chainstatemanager_tests.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index d5d495f97e48..ab778998297d 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -487,9 +487,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init_height_200, SnapshotTest ChainstateManager& restarted = this->SimulateNodeRestart(); this->LoadVerifyActivateChainstate(); - g_txindex = std::make_unique(1 << 20, /*memory=*/true); - BOOST_REQUIRE(g_txindex->Start(restarted.ActiveChainstate())); - IndexWaitSynced(*g_txindex); BOOST_CHECK_EQUAL(WITH_LOCK(restarted.GetMutex(), return restarted.ActiveHeight()), 200); } From 9f74163959f980aac4a16e1b897466b4cd59e64a Mon Sep 17 00:00:00 2001 From: Pasta Date: Tue, 14 Jul 2026 11:25:19 -0500 Subject: [PATCH 25/30] kernel: deduplicate BlockInfo declarations --- src/interfaces/chain.h | 17 ----------------- src/kernel/chain.h | 32 ++++++++++++-------------------- 2 files changed, 12 insertions(+), 37 deletions(-) diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index b83818c1350a..adb34b4e5c94 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -20,7 +20,6 @@ class ArgsManager; class CBlock; -class CBlockUndo; class CFeeRate; class CRPCCommand; class CScheduler; @@ -90,22 +89,6 @@ class FoundBlock mutable bool found = false; }; -//! Block data sent with blockConnected, blockDisconnected notifications. -struct BlockInfo { - const uint256& hash; - const uint256* prev_hash = nullptr; - int height = -1; - int file_number = -1; - unsigned data_pos = 0; - const CBlock* data = nullptr; - const CBlockUndo* undo_data = nullptr; - // The maximum time in the chain up to and including this block. - // A timestamp that can only move forward. - unsigned int chain_time_max{0}; - - BlockInfo(const uint256& hash LIFETIMEBOUND) : hash(hash) {} -}; - //! Interface giving clients (wallet processes, maybe other analysis tools in //! the future) ability to access to the chain state, receive notifications, //! estimate fees, and submit transactions. diff --git a/src/kernel/chain.h b/src/kernel/chain.h index 4996c799551d..50b6f5cf9f2d 100644 --- a/src/kernel/chain.h +++ b/src/kernel/chain.h @@ -5,37 +5,29 @@ #ifndef BITCOIN_KERNEL_CHAIN_H #define BITCOIN_KERNEL_CHAIN_H -#include - -class CBlock; -class CBlockIndex; -namespace interfaces { -struct BlockInfo; -} // namespace interfaces +#include -namespace kernel { -//! Return data from block index. -interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* block_index, const CBlock* data = nullptr); - -} // namespace kernel +#include class CBlock; class CBlockIndex; class CBlockUndo; class uint256; namespace interfaces { -//! Block data sent with blockConnected and blockDisconnected notifications. +//! Block data sent with blockConnected, blockDisconnected notifications. struct BlockInfo { const uint256& hash; - const uint256* prev_hash{nullptr}; - int height{-1}; - int file_number{-1}; - unsigned data_pos{0}; - const CBlock* data{nullptr}; - const CBlockUndo* undo_data{nullptr}; + const uint256* prev_hash = nullptr; + int height = -1; + int file_number = -1; + unsigned data_pos = 0; + const CBlock* data = nullptr; + const CBlockUndo* undo_data = nullptr; + // The maximum time in the chain up to and including this block. + // A timestamp that can only move forward. unsigned int chain_time_max{0}; - explicit BlockInfo(const uint256& block_hash) : hash(block_hash) {} + BlockInfo(const uint256& hash LIFETIMEBOUND) : hash(hash) {} }; } // namespace interfaces From 7da051e1c1c21981a9a645922fe8b3e5d7191ac1 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 20 Aug 2026 00:33:44 -0500 Subject: [PATCH 26/30] fix: align the early populate-time work check message with bitcoin#28562 The bitcoin#28562 replay renamed this message in the (since-removed) evo/snapshot_load.cpp copy of PopulateAndValidateSnapshot; port the rename to the retained validation.cpp copy so both work-comparator failures report identically. Co-Authored-By: Claude Fable 5 --- src/validation.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation.cpp b/src/validation.cpp index cc4c7451e202..67fb986df8c4 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5867,7 +5867,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot( // the active chainstate. ActivateSnapshot repeats this check before the swap // in case the active tip advances while the snapshot is being loaded. if (WITH_LOCK(::cs_main, return !node::CBlockIndexWorkComparator()(ActiveTip(), snapshot_start_block))) { - LogPrintf("[snapshot] activation failed - height does not exceed active chainstate\n"); + LogPrintf("[snapshot] activation failed - work does not exceed active chainstate\n"); return false; } From 0840fa03650d597c340a04c36b375c0460b5e4ca Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 20 Aug 2026 00:38:50 -0500 Subject: [PATCH 27/30] fix: register Dash NodeContext indexes for assumeutxo restart The bitcoin#27596 replay registered the Dash indexes in node.indexes via the pre-#7547 globals, which no longer exist; use the NodeContext members. Fold into the 27596 adaptation commit when the M5 PR is cut. Co-Authored-By: Claude Fable 5 --- src/init.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 6aa4616c6db9..eb8cfa87648b 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -2236,7 +2236,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (!node.address_index->Start()) { return false; } - node.indexes.push_back(g_addressindex.get()); + node.indexes.push_back(node.address_index.get()); } if (args.GetBoolArg("-timestampindex", DEFAULT_TIMESTAMPINDEX)) { @@ -2244,7 +2244,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (!node.timestamp_index->Start()) { return false; } - node.indexes.push_back(g_timestampindex.get()); + node.indexes.push_back(node.timestamp_index.get()); } if (args.GetBoolArg("-spentindex", DEFAULT_SPENTINDEX)) { @@ -2252,7 +2252,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (!node.spent_index->Start()) { return false; } - node.indexes.push_back(g_spentindex.get()); + node.indexes.push_back(node.spent_index.get()); } for (const auto& filter_type : g_enabled_filter_types) { From 0776aacf7c7775d3ab7efa9dd5621d232af64fc5 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 20 Aug 2026 01:08:06 -0500 Subject: [PATCH 28/30] fix: gate the activation-time background MN-list capture on validation status The capture fired only when the background tip pointed exactly at the snapshot base, but what makes the EvoDB lookup safe is that this node fully validated the base block itself. Upstream's CreateAndActivateUTXOSnapshot temporarily rewinds the background tip by one block around ActivateSnapshot, so the tip predicate skipped the capture and completion failed with EVO_STATE_MISMATCH despite a fully validated base. Gate on BLOCK_VALID_SCRIPTS instead and refresh the stale pre-payload comment. Co-Authored-By: Claude Fable 5 --- src/validation.cpp | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index 67fb986df8c4..5865802523b5 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -6115,25 +6115,23 @@ bool ChainstateManager::PopulateAndValidateSnapshot( index->nChainTx = au_data.nChainTx; snapshot_chainstate.setBlockIndexCandidates.insert(snapshot_start_block); - // Until the loadtxoutset milestone the snapshot carries no Dash payload, - // so the base MN list is only derivable when this node's own background - // chainstate has already validated the base block. On a cold start - // (background tip below the base) it is not derivable at all: attempting - // the lookup would take GetListForBlockInternal's legacy bootstrap branch - // (the dual-chainstate marker is not durable yet at this point), fabricate - // an empty "initial snapshot" list for the base block, and poison the - // shared list cache that the background chainstate later derives base+1 - // from. Capture the lifecycle hashes only when the base state genuinely - // exists; completion skips the comparison when the markers are absent. - // The background chainstate never re-connects a base block it has already - // validated, so RecordBackgroundMNListHash cannot fire for it either -- - // this capture stands in for it. - // TODO(assumeutxo, loadtxoutset): once the snapshot payload carries the - // base MN list, derive the SNAPSHOT-side marker from the payload so it is - // always present and independent of local state. + // The BACKGROUND-side lifecycle marker records the base MN list this + // node derived on its own, independent of the seeded snapshot payload. + // It is only derivable here when the background chainstate has already + // validated the base block; a background chainstate that validated the + // base never re-connects it, so RecordBackgroundMNListHash cannot fire + // for it and this capture stands in for it. On a cold start (base never + // validated) it is not derivable at all: attempting the lookup would take + // GetListForBlockInternal's legacy bootstrap branch (the dual-chainstate + // marker is not durable yet at this point), fabricate an empty "initial + // snapshot" list for the base block, and poison the shared list cache + // that the background chainstate later derives base+1 from. There the + // background sync captures the marker when it connects the base itself. std::optional base_mn_list_hash; - if (const CBlockIndex* ibd_tip = m_ibd_chainstate->m_chain.Tip(); - ibd_tip != nullptr && ibd_tip->GetBlockHash() == base_blockhash) { + // What makes the lookup safe is that this node fully validated the base + // block itself, not where the background tip happens to point (the test + // harness temporarily rewinds it around activation). + if (snapshot_start_block->IsValid(BLOCK_VALID_SCRIPTS)) { base_mn_list_hash = snapshot_chainstate.ChainHelper().GetDeterministicMNListHash(snapshot_start_block); auto db_tx = snapshot_chainstate.m_evoDb.BeginTransaction(::EvoDbIdentity::NORMAL); From 372456a8844ca0f6f893c2142c9ebaa0a133e276 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 20 Aug 2026 01:08:06 -0500 Subject: [PATCH 29/30] test: adapt the tampered-evo E2E case to context-free credit-pool validation Flipping currentLimit's low bit now violates the currentLimit <= locked invariant on this fixture (locked is zero), so the corruption is rejected during decoding before the hash comparison the case exists to exercise. Tamper latelyUnlocked instead, and keep the currentLimit flip as explicit coverage of the decode-time semantic rejection. Co-Authored-By: Claude Fable 5 --- test/functional/feature_assumeutxo_dash.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test/functional/feature_assumeutxo_dash.py b/test/functional/feature_assumeutxo_dash.py index 965581ee02b0..b3d7ec758c2a 100755 --- a/test/functional/feature_assumeutxo_dash.py +++ b/test/functional/feature_assumeutxo_dash.py @@ -264,11 +264,12 @@ def run_test(self): # This fixture has no asset-unlock ranges or MNHF signals, so the last # 26 bytes are three int64 credit-pool fields followed by two zero - # CompactSize counts. Alter currentLimit, which remains structurally - # valid and is intentionally not a CbTx root, to reach the evo hash check. + # CompactSize counts. Alter latelyUnlocked, which stays within every + # context-free invariant and is intentionally not a CbTx root, to + # reach the evo hash check. tampered = bytearray(snapshot_bytes) assert_equal(tampered[-2:], b"\x00\x00") - tampered[-18] ^= 1 + tampered[-10] ^= 1 tampered_path = snapshot_path.with_suffix(".tampered-evo.dat") tampered_path.write_bytes(tampered) with negative.assert_debug_log(["bad evo snapshot hash"]): @@ -278,6 +279,21 @@ def run_test(self): negative.loadtxoutset, str(tampered_path), ) + + # Raising currentLimit above locked violates a context-free credit-pool + # invariant, so this corruption is rejected during decoding, before the + # evo hash is ever compared. + semantic = bytearray(snapshot_bytes) + semantic[-18] ^= 1 + semantic_path = snapshot_path.with_suffix(".semantic-evo.dat") + semantic_path.write_bytes(semantic) + with negative.assert_debug_log(["truncated or invalid evo section"]): + assert_raises_rpc_error( + -32603, + "truncated or invalid evo section", + negative.loadtxoutset, + str(semantic_path), + ) self.log.info("Load the full snapshot on a fresh non-masternode") snapshot_index, snapshot_args = self.add_snapshot_node(assumeutxo_arg) snapshot_node = self.nodes[snapshot_index] From 6fbad11705a5551b8f78771d3132713779862302 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 20 Aug 2026 01:19:09 -0500 Subject: [PATCH 30/30] fix: make a missing snapshot base header nonfatal at startup With loadtxoutset a snapshot chainstate legitimately exists before its base header arrives, so the M3-era LoadBlockIndex hard error (and its test) is superseded: candidate admission and CheckBlockIndex already tolerate a null snapshot base, and snapshot_startup_missing_base_header_is_nonfatal covers the replacement behavior. This matches the reviewed pre-decomposition M5 disposition; fold into the load-integration commit when the M5 PR is cut. Co-Authored-By: Claude Fable 5 --- .../validation_chainstatemanager_tests.cpp | 25 ------------------- src/validation.cpp | 15 +++-------- 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index ab778998297d..af6c7e5a2ac2 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -1298,31 +1298,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_base_is_cached, SnapshotTestS } } -//! A snapshot chainstate whose base block is missing from the on-disk block -//! index must fail startup with a recoverable error (reindex discards the -//! snapshot), not abort in candidate admission. -BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_missing_base_fails_load, SnapshotTestSetup) -{ - this->SetupSnapshot(); - this->SimulateNodeRestart(); - - // Simulate a block-tree database that has lost the snapshot's history - // (wiped or swapped blocks/index) while the snapshot chainstate directory - // and its EvoDB marker survive. - fs::remove_all(gArgs.GetDataDirNet() / "blocks" / "index"); - - ChainstateManager& chainman = *Assert(m_node.chainman); - node::ChainstateLoadStatus status; - bilingual_str error; - { - ASSERT_DEBUG_LOG("missing from the block index"); - std::tie(status, error) = node::LoadChainstate(chainman, m_cache_sizes, ChainstateLoadOptionsForTest(), - *m_node.evodb, *m_node.dmnman, m_node.llmq_ctx, - m_node.chain_helper); - } - BOOST_CHECK(status == node::ChainstateLoadStatus::FAILURE); -} - BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion_without_base_list_marker, SnapshotTestSetup) { this->SetupSnapshot(); diff --git a/src/validation.cpp b/src/validation.cpp index 5865802523b5..5598b385c44c 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4956,17 +4956,10 @@ bool ChainstateManager::LoadBlockIndex() m_blockman.ScanAndUnlinkAlreadyPrunedFiles(); - // Candidate admission below Asserts the snapshot base for the - // background chainstate, so a base missing from the on-disk block - // index must be reported here, as a recoverable startup error, before - // any admission runs. -reindex discards the snapshot chainstate and - // its EvoDB markers, so the standard rebuild advice recovers. - if (const auto base_hash{SnapshotBlockhash()}) { - if (!m_blockman.LookupBlockIndex(*base_hash)) { - return error("[snapshot] base block %s of the active snapshot chainstate is missing from the block index", - base_hash->ToString()); - } - } + // A snapshot chainstate may legitimately exist before its base header + // has arrived (a loadtxoutset node restarted before header sync), so a + // base missing from the block index is not a startup error: candidate + // admission and CheckBlockIndex tolerate a null snapshot base. std::vector vSortedByHeight{m_blockman.GetAllBlockIndices()}; std::sort(vSortedByHeight.begin(), vSortedByHeight.end(),