diff --git a/contrib/devtools/test_utxo_snapshots.sh b/contrib/devtools/test_utxo_snapshots.sh new file mode 100755 index 000000000000..3d1da706e40e --- /dev/null +++ b/contrib/devtools/test_utxo_snapshots.sh @@ -0,0 +1,203 @@ +#!/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 dashd 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/dash-cli -rpcport=$SERVER_RPC_PORT -datadir="$SERVER_DATADIR" "$@" +} +client_rpc() { + ./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 +} +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/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/dashd -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_EVO=$(jq -r .evo_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/chainparams.cpp, and recompile:" +echo +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/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/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 + +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/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 + +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/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 " +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/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/dash-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat" + +echo +echo "-- Done!" 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/design/assumeutxo.md b/doc/design/assumeutxo.md index 9846f7f26a0b..1ff6b3eae79c 100644 --- a/doc/design/assumeutxo.md +++ b/doc/design/assumeutxo.md @@ -1,10 +1,46 @@ # 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` (yet to be merged) 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 diff --git a/doc/release-notes-27596.md b/doc/release-notes-27596.md new file mode 100644 index 000000000000..7c74d36d47f2 --- /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. + +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 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 (). + +`getchainstates` has been added to aid in monitoring the assumeutxo sync process. 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/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..d25016360f38 100644 --- a/src/chain.h +++ b/src/chain.h @@ -261,8 +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. */ - bool HaveTxsDownloaded() const { return nChainTx != 0; } + bool HaveNumChainTxs() const { return nChainTx != 0; } NodeSeconds Time() const { 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 565d2486bd97..9e04f8fa1d79 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,17 +879,62 @@ class CRegTestParams : public CChainParams { } }; - m_assumeutxo_data = MapAssumeutxo{ + m_assumeutxo_data = { { - 110, - {AssumeutxoHash{uint256S("0x9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b")}, EvoSnapshotHash{uint256{}}, 110}, + .height = 110, + .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{}}, + .nChainTx = 111, + .blockhash = uint256S("0x729bcb1479ff9f4968439f0276bd76bcb2de0f0720b7a16f383321f6a41cb238"), }, { - 200, - {AssumeutxoHash{uint256S("0x8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3")}, EvoSnapshotHash{uint256{}}, 200}, + .height = 200, + .hash_serialized = AssumeutxoHash{uint256S("0x16e00a64db4fa48dd989dce86d8677f41797d52044e5fc86021aa88cc22b665b")}, + .evo_hash = EvoSnapshotHash{uint256{}}, + .nChainTx = 201, + .blockhash = uint256S("0x19c1b203b5a960c7f3619e0e805b24c94e684b1aa261f3a79c84d291638a6e1f"), + }, + { + // 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("0xd7f46f9830ea11f1bfc565b08f63b66f09e1403b54c988ede40461cf0846fcba")}, + .evo_hash = EvoSnapshotHash{uint256S("0xf2ccd3fef604df58a0c174489821e16912c9332969a267650cd040e85fb2adde")}, + .nChainTx = 300, + .blockhash = uint256S("0x64ce3ab60754c7974ea472221fa9c7a04f2d193ba480d1ef61b5d12216b14760"), }, }; + 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, @@ -1392,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); 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/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 b52ff2d11055..0de3291ceb39 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 { @@ -88,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)) { @@ -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..bc79b29fd759 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; } @@ -123,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); 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/init.cpp b/src/init.cpp index d083d168270f..eb8cfa87648b 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); @@ -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(node.address_index.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(node.timestamp_index.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(node.spent_index.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/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 de2ce3bd3bb3..adb34b4e5c94 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 @@ -19,7 +20,6 @@ class ArgsManager; class CBlock; -class CBlockUndo; class CFeeRate; class CRPCCommand; class CScheduler; @@ -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; @@ -87,19 +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; - - 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. @@ -302,10 +291,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 82e77125d7f3..a248e4dc3f1d 100644 --- a/src/kernel/chain.cpp +++ b/src/kernel/chain.cpp @@ -3,12 +3,11 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include -#include +#include #include #include class CBlock; - namespace kernel { interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* index, const CBlock* data) { @@ -16,6 +15,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; @@ -24,3 +24,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..50b6f5cf9f2d 100644 --- a/src/kernel/chain.h +++ b/src/kernel/chain.h @@ -5,15 +5,53 @@ #ifndef BITCOIN_KERNEL_CHAIN_H #define BITCOIN_KERNEL_CHAIN_H +#include + +#include + class CBlock; class CBlockIndex; +class CBlockUndo; +class uint256; namespace interfaces { -struct BlockInfo; +//! 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) {} +}; } // 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. +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/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/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"); } diff --git a/src/net_processing.cpp b/src/net_processing.cpp index e25a9a1230c5..d1f7fdf78d4c 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->HaveNumChainTxs()) 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,31 @@ 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)); + } + } + + // The following task can be skipped since we don't maintain a mempool for + // the ibd/background chainstate. + 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 +2192,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) @@ -2672,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 @@ -6688,7 +6742,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..6b710e1784eb 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,62 @@ 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. On regtest, preserve + // the lower configured target used by pruning tests. + const auto target = std::max( + std::min(MIN_DISK_SPACE_FOR_BLOCK_FILES, nPruneTarget), + 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 +228,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 (chain.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 +264,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 +297,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 +318,30 @@ bool BlockManager::LoadBlockIndex() } } + if (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)}; + + // 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 +363,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 +404,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 +450,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 +482,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 +639,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 +659,45 @@ 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 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) { + return FlushBlockFile(cursor->file_num, /*fFinalize=*/false, /*finalize_undo=*/false); + } + // No need to log warnings in this case. + return true; } uint64_t BlockManager::CalculateCurrentUsage() @@ -659,8 +756,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 +785,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 +806,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 +897,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 +914,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 +1092,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/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. 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/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..3be02ea8aeae 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -1139,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; @@ -1181,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."}, }, @@ -1192,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)"}, @@ -1251,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) { @@ -1280,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()); @@ -1851,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)) { @@ -3170,6 +3171,185 @@ 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 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 ().", + { + {"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); + if (node.active_ctx) { + throw JSONRPCError(RPC_MISC_ERROR, + "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")}; + AutoFile afile{file}; + if (afile.IsNull()) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "Couldn't open file " + fs::PathToString(path) + " for reading."); + } + + SnapshotMetadata metadata; + 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()); + + 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"); + } + 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())}; + + 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"}, + {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() +{ +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::ARR, "chainstates", "list of the chainstates ordered by work, with the most-work (active) chainstate last", {{RPCResult::Type::OBJ, "", "", RPCHelpForChainstate},}}, + } + }, + RPCExamples{ + HelpExampleCli("getchainstates", "") + + HelpExampleRpc("getchainstates", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + LOCK(cs_main); + UniValue obj(UniValue::VOBJ); + + ChainstateManager& chainman = EnsureAnyChainman(request.context); + + 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()) { + 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()); + } + data.pushKV("validated", validated); + return data; + }; + + 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; +} + }; +} + + void RegisterBlockchainRPCCommands(CRPCTable& t) { static const CRPCCommand commands[]{ @@ -3198,13 +3378,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/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/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_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 5b466a722f5f..af6c7e5a2ac2 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -51,23 +51,7 @@ void SeedSnapshotMarker(CEvoDB& evodb, const uint256& hash) } // namespace -BOOST_FIXTURE_TEST_SUITE(validation_chainstatemanager_tests, ChainTestingSetup) - -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)); -} +BOOST_FIXTURE_TEST_SUITE(validation_chainstatemanager_tests, TestingSetup) static void DashChainstateSetupClose(node::NodeContext& node) { @@ -78,10 +62,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 +72,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,41 +85,36 @@ 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); BOOST_CHECK(!manager.SnapshotBlockhash().has_value()); - DashChainstateSetupClose(m_node); - // Create a snapshot-based chainstate. // 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. - 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); - 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,38 +127,31 @@ 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(); - 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. @@ -200,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) @@ -229,7 +187,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 +198,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 +211,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 +363,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 @@ -521,6 +473,24 @@ 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(); + + BOOST_CHECK_EQUAL(WITH_LOCK(restarted.GetMutex(), return restarted.ActiveHeight()), 200); +} + //! Test basic snapshot activation. BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, SnapshotTestSetup) { @@ -589,18 +559,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 +602,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 +628,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 +639,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 +676,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); } @@ -873,6 +891,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; @@ -889,10 +913,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(); @@ -1278,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/test/validation_tests.cpp b/src/test/validation_tests.cpp index 60263280834a..a8fc2d6c3069 100644 --- a/src/test/validation_tests.cpp +++ b/src/test/validation_tests.cpp @@ -27,17 +27,21 @@ 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); - BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "9b2a277a3e3b979f1a539d57e949495d7f8247312dbc32bce6619128c192b44b"); - BOOST_CHECK_EQUAL(out110.nChainTx, 110U); + const auto out110 = *params->AssumeutxoForHeight(110); + BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "ffb210087e1ed14526c0c08a3ec3a7c8e288079eaa68acb87d3d4d9fd746079f"); + BOOST_CHECK_EQUAL(out110.nChainTx, 111U); - const auto out210 = *ExpectedAssumeutxo(200, *params); - BOOST_CHECK_EQUAL(out210.hash_serialized.ToString(), "8a5bdd92252fc6b24663244bbe958c947bb036dc1f94ccd15439f48d8d1cb4e3"); - BOOST_CHECK_EQUAL(out210.nChainTx, 200U); + const auto out110_2 = *params->AssumeutxoForBlockhash(uint256S("0x729bcb1479ff9f4968439f0276bd76bcb2de0f0720b7a16f383321f6a41cb238")); + 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(), "16e00a64db4fa48dd989dce86d8677f41797d52044e5fc86021aa88cc22b665b"); + BOOST_CHECK_EQUAL(out210.nChainTx, 201U); } //! Test the Dash (non-witness) IsBlockMutated() predicate directly. 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 3f32df6aeca8..5598b385c44c 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()); @@ -3174,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 @@ -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 = 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 = 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; } @@ -3516,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(); } @@ -3565,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)); } } @@ -3665,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); } } @@ -3769,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++; @@ -3862,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); } @@ -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; } @@ -3906,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; @@ -3915,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->HaveTxsDownloaded()) { + 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); @@ -4522,6 +4564,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,22 +4951,15 @@ 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(); - // 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(), @@ -4933,7 +4974,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); @@ -5231,6 +5272,16 @@ void ChainstateManager::CheckBlockIndex() CBlockIndex* pindexFirstAssumeValid = nullptr; // Oldest ancestor of pindex which has BLOCK_ASSUMED_VALID while (pindex != nullptr) { nNodes++; + // 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) + // 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; @@ -5270,7 +5321,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 @@ -5300,9 +5351,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. @@ -5538,19 +5589,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); @@ -5606,7 +5645,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; @@ -5615,6 +5655,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 +5712,9 @@ 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 std::string& reason) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + LogPrintf("[snapshot] activation failed - %s\n", reason); + if (error) *error = reason; this->ReleaseSnapshotPruneLock(); this->MaybeRebalanceCaches(); @@ -5709,35 +5747,50 @@ bool ChainstateManager::ActivateSnapshot( } } return false; - } + }; - { + std::string population_error; + if (!this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata, &population_error)) { 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_error.empty() ? "population failed" : population_error); + } - 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; } @@ -5768,7 +5821,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. @@ -5779,7 +5833,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 +5846,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 +5856,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 !node::CBlockIndexWorkComparator()(ActiveTip(), snapshot_start_block))) { + LogPrintf("[snapshot] activation failed - work does not exceed active chainstate\n"); + return false; + } + COutPoint outpoint; Coin coin; const uint64_t coins_count = metadata.m_coins_count; @@ -5826,6 +5888,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)); @@ -5877,12 +5944,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 { @@ -5891,12 +5960,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. @@ -5905,6 +5976,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; } @@ -5948,6 +6020,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; } } @@ -5957,12 +6030,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; } @@ -5975,10 +6051,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)) { @@ -6030,25 +6108,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); @@ -6324,7 +6400,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 +6713,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 +6750,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 +6797,23 @@ 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; +} + const CBlockIndex* ChainstateManager::GetSnapshotBaseBlock() const { return m_active_chainstate ? m_active_chainstate->SnapshotBase() : nullptr; @@ -6818,3 +6922,48 @@ 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; +} + +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 3e85ff3b90af..3012ff7e3dc4 100644 --- a/src/validation.h +++ b/src/validation.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -544,6 +545,12 @@ class Chainstate 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); + /** * Initialize the CoinsViews UTXO set database management data structures. The in-memory * cache is initialized separately. @@ -890,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 @@ -901,20 +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. - //! - //! 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. Chainstate* m_active_chainstate GUARDED_BY(::cs_main) {nullptr}; CBlockIndex* m_best_invalid GUARDED_BY(::cs_main){nullptr}; @@ -923,7 +925,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 @@ -938,13 +941,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 @@ -961,6 +957,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(); } @@ -1069,7 +1069,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 @@ -1091,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); @@ -1252,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 @@ -1266,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(); }; @@ -1299,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..f2f4ff83ef35 --- /dev/null +++ b/test/functional/feature_assumeutxo.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +# 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 +a serialized version of the UTXO set at a certain height, which corresponds +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`. + +## 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: Valid hash but invalid snapshot file (bad coin height or + bad other serialization) +- 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.governance import EXPECTED_STDERR_NO_GOV_PRUNE +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) + +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 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() + + 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})" + 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") + 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_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, "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: + 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 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) + + 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, + # 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, + 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() + + # 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=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) + 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'], + '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.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) + 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) + + 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]], + expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) + + # 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) + + 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], expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) + self.connect_nodes(0, 1) + + self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})") + + def check_for_final_height(): + chainstates = n1.getchainstates()['chainstates'] + # Background validation may complete before the first check, so + # 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") + self.wait_until(lambda: len(n1.getchainstates()['chainstates']) == 1) + + # 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], + expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE if i == 1 else '') + + assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT) + + chainstate, = n.getchainstates()['chainstates'] + assert_equal(chainstate['blocks'], FINAL_HEIGHT) + + 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) + + 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.test_invalid_chainstate_scenarios(2) + + self.connect_nodes(0, 2) + 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: len(n2.getchainstates()['chainstates']) == 1) + + 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) + + chainstate, = n.getchainstates()['chainstates'] + assert_equal(chainstate['blocks'], FINAL_HEIGHT) + + 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], + '-txindex=0', '-blockfilterindex=0', '-coinstatsindex=0']) + assert_equal(n2.getblockchaininfo()["blocks"], 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) + self.wait_until(lambda: n2.getblockcount() == FINAL_HEIGHT) + + self.stop_node(1, expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) + + +if __name__ == '__main__': + AssumeutxoTest().main() diff --git a/test/functional/feature_assumeutxo_dash.py b/test/functional/feature_assumeutxo_dash.py index ed5c43509bba..b3d7ec758c2a 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,344 @@ 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 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[-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"]): + assert_raises_rpc_error( + -32603, + "evo snapshot hash mismatch", + 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] + # 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__": 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) 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 eb00105ca1b8..5070af1279b5 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, ) @@ -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'] @@ -905,7 +915,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. @@ -1163,6 +1173,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 @@ -1507,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) 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: diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 88c12d0a225e..7413b7aa2ec8 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -379,6 +379,8 @@ 'wallet_coinbase_category.py --descriptors', '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() 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 = [