diff --git a/contrib/devtools/utxo_snapshot.sh b/contrib/devtools/utxo_snapshot.sh deleted file mode 100755 index 2d8583f9657c..000000000000 --- a/contrib/devtools/utxo_snapshot.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (c) 2019-2023 The Bitcoin Core developers -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -# -export LC_ALL=C - -set -ueo pipefail - -NETWORK_DISABLED=false - -if (( $# < 3 )); then - echo 'Usage: utxo_snapshot.sh ' - echo - echo " if is '-', don't produce a snapshot file but instead print the " - echo " expected assumeutxo hash" - echo - echo 'Examples:' - echo - echo " ./contrib/devtools/utxo_snapshot.sh 570000 utxo.dat ./src/dash-cli -datadir=\$(pwd)/testdata" - echo ' ./contrib/devtools/utxo_snapshot.sh 570000 - ./src/dash-cli' - exit 1 -fi - -GENERATE_AT_HEIGHT="${1}"; shift; -OUTPUT_PATH="${1}"; shift; -# Most of the calls we make take a while to run, so pad with a lengthy timeout. -BITCOIN_CLI_CALL="${*} -rpcclienttimeout=9999999" - -# Check if the node is pruned and get the pruned block height -PRUNED=$( ${BITCOIN_CLI_CALL} getblockchaininfo | awk '/pruneheight/ {print $2}' | tr -d ',' ) - -if (( GENERATE_AT_HEIGHT < PRUNED )); then - echo "Error: The requested snapshot height (${GENERATE_AT_HEIGHT}) should be greater than the pruned block height (${PRUNED})." - exit 1 -fi - -# Check current block height to ensure the node has synchronized past the required block -CURRENT_BLOCK_HEIGHT=$(${BITCOIN_CLI_CALL} getblockcount) -PIVOT_BLOCK_HEIGHT=$(( GENERATE_AT_HEIGHT + 1 )) - -if (( PIVOT_BLOCK_HEIGHT > CURRENT_BLOCK_HEIGHT )); then - (>&2 echo "Error: The node has not yet synchronized to block height ${PIVOT_BLOCK_HEIGHT}.") - (>&2 echo "Please wait until the node has synchronized past this block height and try again.") - exit 1 -fi - -# Early exit if file at OUTPUT_PATH already exists -if [[ -e "$OUTPUT_PATH" ]]; then - (>&2 echo "Error: $OUTPUT_PATH already exists or is not a valid path.") - exit 1 -fi - -# Validate that the path is correct -if [[ "${OUTPUT_PATH}" != "-" && ! -d "$(dirname "${OUTPUT_PATH}")" ]]; then - (>&2 echo "Error: The directory $(dirname "${OUTPUT_PATH}") does not exist.") - exit 1 -fi - -function cleanup { - (>&2 echo "Restoring chain to original height; this may take a while") - ${BITCOIN_CLI_CALL} reconsiderblock "${PIVOT_BLOCKHASH}" - - if $NETWORK_DISABLED; then - (>&2 echo "Restoring network activity") - ${BITCOIN_CLI_CALL} setnetworkactive true - fi -} - -function early_exit { - (>&2 echo "Exiting due to Ctrl-C") - cleanup - exit 1 -} - -# Prompt the user to disable network activity -read -p "Do you want to disable network activity (setnetworkactive false) before running invalidateblock? (Y/n): " -r -if [[ "$REPLY" =~ ^[Yy]*$ || -z "$REPLY" ]]; then - # User input is "Y", "y", or Enter key, proceed with the action - NETWORK_DISABLED=true - (>&2 echo "Disabling network activity") - ${BITCOIN_CLI_CALL} setnetworkactive false -else - (>&2 echo "Network activity remains enabled") -fi - -# Block we'll invalidate/reconsider to rewind/fast-forward the chain. -PIVOT_BLOCKHASH=$($BITCOIN_CLI_CALL getblockhash $(( GENERATE_AT_HEIGHT + 1 )) ) - -# Trap for normal exit and Ctrl-C -trap cleanup EXIT -trap early_exit INT - -(>&2 echo "Rewinding chain back to height ${GENERATE_AT_HEIGHT} (by invalidating ${PIVOT_BLOCKHASH}); this may take a while") -${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' -else - (>&2 echo "Generating UTXO snapshot...") - ${BITCOIN_CLI_CALL} dumptxoutset "${OUTPUT_PATH}" -fi diff --git a/doc/assumeutxo.md b/doc/assumeutxo.md new file mode 100644 index 000000000000..d5da07d6c39b --- /dev/null +++ b/doc/assumeutxo.md @@ -0,0 +1,85 @@ +# Assumeutxo Usage + +Assumeutxo is a feature that allows fast bootstrapping of a validating dashd +instance. + +For notes on the design of Assumeutxo, please refer to [the design doc](/doc/design/assumeutxo.md). + +## 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. If there is +no source for the snapshot you need, you can generate it yourself using +`dumptxoutset` on another node that is already synced (see +[Generating a snapshot](#generating-a-snapshot)). + +Once you've obtained the snapshot, you can use the RPC command `loadtxoutset` to +load it. + +``` +$ dash-cli loadtxoutset /path/to/input +``` + +After the snapshot has loaded, the syncing process of both the snapshot chain +and the background IBD chain can be monitored with the `getchainstates` RPC. + +### 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 `-prune` 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 +downloaded snapshot). + +### Indexes + +Indexes work but don't take advantage of this feature. They always start building +from the genesis block and can only apply blocks in order. 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 these indexes only allow blocks that +were already indexed to be pruned. Blocks that are not indexed yet will also +not be pruned. + +This means that, if the snapshot is old, then a lot of blocks after the snapshot +block will need to be downloaded, and these blocks can't be pruned until they +are indexed, so they could consume a lot of disk space until indexing catches up +to the snapshot block. + +## Generating a snapshot + +The RPC command `dumptxoutset` can be used to generate a snapshot for the current +tip (using type "latest") or a recent height (using type "rollback"). A generated +snapshot from one node can then be loaded +on any other node. However, keep in mind that the snapshot hash needs to be +listed in the chainparams to make it usable. If there is no snapshot hash for +the height you have chosen already, you will need to change the code there and +re-compile. + +Using the type parameter "rollback", `dumptxoutset` can also be used to verify the +hardcoded snapshot hash in the source code by regenerating the snapshot and +comparing the hash. + +Example usage: + +``` +$ dash-cli -rpcclienttimeout=0 dumptxoutset /path/to/output rollback +``` + +For most of the duration of `dumptxoutset` running the node is in a temporary +state that does not actually reflect reality, i.e. blocks are marked invalid +although we know they are not invalid. Because of this it is discouraged to +interact with the node in any other way during this time to avoid inconsistent +results and race conditions, particularly RPCs that interact with blockstorage. +This inconsistent state is also why network activity is temporarily disabled, +causing us to disconnect from all peers. + +`dumptxoutset` takes some time to complete, independent of hardware and +what parameter is chosen. Because of that it is recommended to increase the RPC +client timeout value (use `-rpcclienttimeout=0` for no timeout). diff --git a/doc/design/assumeutxo.md b/doc/design/assumeutxo.md index 74aff7d392f8..ba6f1a4926e8 100644 --- a/doc/design/assumeutxo.md +++ b/doc/design/assumeutxo.md @@ -1,11 +1,6 @@ -# assumeutxo +# Assumeutxo Design -Assumeutxo is a feature that allows fast bootstrapping of a validating dashd -instance with a very similar security model to assumevalid. - -The RPC commands `dumptxoutset` and `loadtxoutset` are used to respectively generate -and load UTXO snapshots. The utility script `./contrib/devtools/utxo_snapshot.sh` may -be of use. +For notes on the usage of Assumeutxo, please refer to [the usage doc](/doc/assumeutxo.md). ## General background @@ -15,23 +10,12 @@ be of use. ## Design notes -- A new block index `nStatus` flag is introduced, `BLOCK_ASSUMED_VALID`, to mark block - index entries that are required to be assumed-valid by a chainstate created - from a UTXO snapshot. This flag is mostly used as a way to modify certain - CheckBlockIndex() logic to account for index entries that are pending validation by a - chainstate running asynchronously in the background. We also use this flag to control - which index entries are added to setBlockIndexCandidates during LoadBlockIndex(). - -- Indexing implementations via BaseIndex can no longer assume that indexation happens - sequentially, since background validation chainstates can submit BlockConnected - events out of order with the active chain. - - The concept of UTXO snapshots is treated as an implementation detail that lives behind the ChainstateManager interface. The external presentation of the changes required to facilitate the use of UTXO snapshots is the understanding that there are - now certain regions of the chain that can be temporarily assumed to be valid (using - the nStatus flag mentioned above). In certain cases, e.g. wallet rescanning, this is - very similar to dealing with a pruned chain. + now certain regions of the chain that can be temporarily assumed to be valid. + In certain cases, e.g. wallet rescanning, this is very similar to dealing with + a pruned chain. Logic outside ChainstateManager should try not to know about snapshots, instead preferring to work in terms of more general states like assumed-valid. @@ -54,7 +38,7 @@ data. ### "Normal" operation via initial block download `ChainstateManager` manages a single Chainstate object, for which -`m_snapshot_blockhash` is null. This chainstate is (maybe obviously) +`m_from_snapshot_blockhash` is `std::nullopt`. This chainstate is (maybe obviously) considered active. This is the "traditional" mode of operation for dashd. | | | @@ -76,9 +60,15 @@ original chainstate remains in use as active. Once the snapshot chainstate is loaded and validated, it is promoted to active chainstate and a sync to tip begins. A new chainstate directory is created in the -datadir for the snapshot chainstate called `chainstate_snapshot`. When this directory -is present in the datadir, the snapshot chainstate will be detected and loaded as -active on node startup (via `DetectSnapshotChainstate()`). +datadir for the snapshot chainstate called `chainstate_snapshot`. + +When this directory is present in the datadir, the snapshot chainstate will be detected +and loaded as active on node startup (via `DetectSnapshotChainstate()`). + +A special file is created within that directory, `base_blockhash`, which contains the +serialized `uint256` of the base block of the snapshot. This is used to reinitialize +the snapshot chainstate on subsequent inits. Otherwise, the directory is a normal +leveldb database. | | | | ---------- | ----------- | @@ -88,7 +78,7 @@ active on node startup (via `DetectSnapshotChainstate()`). The snapshot begins to sync to tip from its base block, technically in parallel with the original chainstate, but it is given priority during block download and is allocated most of the cache (see `MaybeRebalanceCaches()` and usages) as our chief -consideration is getting to network tip. +goal is getting to network tip. **Failure consideration:** if shutdown happens at any point during this phase, both chainstates will be detected during the next init and the process will resume. @@ -107,33 +97,32 @@ sequentially. ### Background chainstate hits snapshot base block Once the tip of the background chainstate hits the base block of the snapshot -chainstate, we stop use of the background chainstate by setting `m_stop_use` (not yet -committed - see bitcoin#15606), in `CompleteSnapshotValidation()`, which is checked in -`ActivateBestChain()`). We hash the background chainstate's UTXO set contents and -ensure it matches the compiled value in `CMainParams::m_assumeutxo_data`. - -The background chainstate data lingers on disk until shutdown, when in -`ChainstateManager::Reset()`, the background chainstate is cleaned up with -`ValidatedSnapshotShutdownCleanup()`, which renames the `chainstate_[hash]` datadir as -`chainstate`. +chainstate, we stop use of the background chainstate by setting `m_disabled`, in +`CompleteSnapshotValidation()`, which is checked in `ActivateBestChain()`). We hash the +background chainstate's UTXO set contents and ensure it matches the compiled value in +`CMainParams::m_assumeutxo_data`. | | | | ---------- | ----------- | -| number of chainstates | 2 (ibd has `m_stop_use=true`) | +| number of chainstates | 2 (ibd has `m_disabled=true`) | | active chainstate | snapshot | -**Failure consideration:** if dashd unexpectedly halts after `m_stop_use` is set on -the background chainstate but before `CompleteSnapshotValidation()` can finish, the -need to complete snapshot validation will be detected on subsequent init by -`ChainstateManager::CheckForUncleanShutdown()`. +The background chainstate data lingers on disk until the program is restarted. ### Dashd restarts sometime after snapshot validation has completed -When dashd initializes again, what began as the snapshot chainstate is now -indistinguishable from a chainstate that has been built from the traditional IBD -process, and will be initialized as such. +After a shutdown and subsequent restart, `LoadChainstate()` cleans up the background +chainstate with `ValidatedSnapshotCleanup()`, which renames the `chainstate_snapshot` +datadir as `chainstate` and removes the now unnecessary background chainstate data. | | | | ---------- | ----------- | | number of chainstates | 1 | -| active chainstate | ibd | +| active chainstate | ibd (was snapshot, but is now fully validated) | + +What began as the snapshot chainstate is now indistinguishable from a chainstate that +has been built from the traditional IBD process, and will be initialized as such. + +A file will be left in `chainstate/base_blockhash`, which indicates that the +chainstate, even though now fully validated, was originally started from a snapshot +with the corresponding base blockhash. 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/Makefile.am b/src/Makefile.am index 501a695cf852..1637e93d2646 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -227,9 +227,11 @@ BITCOIN_CORE_H = \ evo/mnauth.h \ evo/mnhftx.h \ evo/netinfo.h \ + evo/snapshot_types.h \ evo/providertx.h \ evo/simplifiedmns.h \ evo/smldiff.h \ + evo/snapshot.h \ evo/specialtx.h \ evo/specialtx_filter.h \ evo/specialtxman.h \ @@ -534,6 +536,8 @@ libbitcoin_node_a_SOURCES = \ evo/evodb.cpp \ evo/mnauth.cpp \ evo/mnhftx.cpp \ + evo/snapshot.cpp \ + evo/snapshot_load.cpp \ evo/providertx.cpp \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ @@ -1271,6 +1275,7 @@ libdashkernel_la_SOURCES = \ evo/providertx_util.cpp \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ + evo/snapshot.cpp \ evo/specialtx.cpp \ evo/specialtx_filter.cpp \ evo/specialtxman.cpp \ @@ -1375,6 +1380,7 @@ libdashkernel_la_SOURCES = \ util/threadnames.cpp \ util/time.cpp \ util/tokenpipe.cpp \ + evo/snapshot_load.cpp \ validation.cpp \ validationinterface.cpp \ versionbits.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index faaee5aa1913..7e9d368dd165 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -110,11 +110,13 @@ BITCOIN_TESTS =\ test/dynamic_activation_thresholds_tests.cpp \ test/evo_assetlocks_tests.cpp \ test/evo_cbtx_tests.cpp \ + test/evo_db_tests.cpp \ test/evo_deterministicmns_tests.cpp \ test/evo_islock_tests.cpp \ test/evo_mnhf_tests.cpp \ test/evo_netinfo_tests.cpp \ test/evo_simplifiedmns_tests.cpp \ + test/evo_snapshot_tests.cpp \ test/evo_trivialvalidation.cpp \ test/evo_utils_tests.cpp \ test/flatfile_tests.cpp \ diff --git a/src/active/context.cpp b/src/active/context.cpp index c77e38fb147d..a92c782c3b14 100644 --- a/src/active/context.cpp +++ b/src/active/context.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ ActiveContext::ActiveContext(CBLSWorker& bls_worker, ChainstateManager& chainman const CBLSSecretKey& operator_sk, const util::DbWrapperParams& db_params, bool quorums_watch) : llmq::QuorumRole{qman}, m_bls_worker{bls_worker}, + m_chainman{chainman}, m_quorums_watch{quorums_watch}, nodeman{std::make_unique(connman, dmnman, operator_sk)}, dkgdbgman{std::make_unique(dmnman, qsnapman, chainman)}, @@ -94,6 +96,17 @@ void ActiveContext::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIn return; nodeman->UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); + + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + if (!m_snapshot_duty_blocked.exchange(true)) { + LogPrintf("Masternode DKG participation and quorum signing are disabled until snapshot background validation completes\n"); + } + return; + } + if (m_snapshot_duty_blocked.exchange(false)) { + LogPrintf("Snapshot background validation completed; masternode DKG participation and quorum signing are enabled\n"); + } + ehf_sighandler->UpdatedBlockTip(pindexNew); gov_signer->UpdatedBlockTip(pindexNew); qdkgsman->UpdatedBlockTip(pindexNew, fInitialDownload); diff --git a/src/active/context.h b/src/active/context.h index 75d092920c4a..ecff35a188cf 100644 --- a/src/active/context.h +++ b/src/active/context.h @@ -12,6 +12,7 @@ #include #include +#include #include class CActiveMasternodeManager; @@ -49,7 +50,9 @@ struct DbWrapperParams; struct ActiveContext final : public llmq::QuorumRole, public CValidationInterface { private: CBLSWorker& m_bls_worker; + ChainstateManager& m_chainman; const bool m_quorums_watch{false}; + std::atomic_bool m_snapshot_duty_blocked{false}; public: ActiveContext() = delete; diff --git a/src/active/dkgsessionhandler.cpp b/src/active/dkgsessionhandler.cpp index 8ea565e8f53f..56cfedf0d42f 100644 --- a/src/active/dkgsessionhandler.cpp +++ b/src/active/dkgsessionhandler.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace llmq { ActiveDKGSessionHandler::ActiveDKGSessionHandler( @@ -41,6 +42,8 @@ ActiveDKGSessionHandler::~ActiveDKGSessionHandler() = default; void ActiveDKGSessionHandler::UpdatedBlockTip(const CBlockIndex* pindexNew) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) return; + //AssertLockNotHeld(cs_main); //Indexed quorums (greater than 0) are enabled with Quorum Rotation if (quorumIndex > 0 && !IsQuorumRotationEnabled(params, pindexNew)) { @@ -76,6 +79,10 @@ std::pair ActiveDKGSessionHandler::GetPhaseAndQuorumHash() bool ActiveDKGSessionHandler::InitNewQuorum(gsl::not_null pQuorumBaseBlockIndex) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "%s -- refusing DKG participation while snapshot background validation is incomplete\n", __func__); + return false; + } if (!DeploymentDIP0003Enforced(pQuorumBaseBlockIndex->nHeight, Params().GetConsensus())) { return false; } @@ -100,6 +107,10 @@ void ActiveDKGSessionHandler::WaitForNextPhase(std::optional curPha LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - starting, curPhase=%d, nextPhase=%d\n", __func__, params.name, quorumIndex, curPhase.has_value() ? std23::to_underlying(*curPhase) : -1, std23::to_underlying(nextPhase)); while (true) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting because snapshot background validation is incomplete\n", __func__, params.name, quorumIndex); + throw AbortPhaseException(); + } if (stopRequested) { LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting due to stop/shutdown requested\n", __func__, params.name, quorumIndex); throw AbortPhaseException(); @@ -139,6 +150,10 @@ void ActiveDKGSessionHandler::WaitForNewQuorum(const uint256& oldQuorumHash) con LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d]- starting\n", __func__, params.name, quorumIndex); while (true) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting because snapshot background validation is incomplete\n", __func__, params.name, quorumIndex); + throw AbortPhaseException(); + } if (stopRequested) { LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting due to stop/shutdown requested\n", __func__, params.name, quorumIndex); throw AbortPhaseException(); @@ -186,6 +201,10 @@ void ActiveDKGSessionHandler::SleepBeforePhase(QuorumPhase curPhase, const uint2 LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - starting sleep for %d ms, curPhase=%d\n", __func__, params.name, quorumIndex, sleepTime, std23::to_underlying(curPhase)); while (SteadyClock::now() < endTime) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting because snapshot background validation is incomplete\n", __func__, params.name, quorumIndex); + throw AbortPhaseException(); + } if (stopRequested) { LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting due to stop/shutdown requested\n", __func__, params.name, quorumIndex); throw AbortPhaseException(); @@ -220,6 +239,10 @@ void ActiveDKGSessionHandler::HandlePhase(QuorumPhase curPhase, QuorumPhase next LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - starting, curPhase=%d, nextPhase=%d\n", __func__, params.name, quorumIndex, std23::to_underlying(curPhase), std23::to_underlying(nextPhase)); SleepBeforePhase(curPhase, expectedQuorumHash, randomSleepFactor, runWhileWaiting); + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "%s -- refusing DKG participation while snapshot background validation is incomplete\n", __func__); + throw AbortPhaseException(); + } startPhaseFunc(); WaitForNextPhase(curPhase, nextPhase, expectedQuorumHash, runWhileWaiting); diff --git a/src/active/masternode.cpp b/src/active/masternode.cpp index 4f3979dc629a..d62514531c0b 100644 --- a/src/active/masternode.cpp +++ b/src/active/masternode.cpp @@ -178,8 +178,19 @@ void CActiveMasternodeManager::UpdatedBlockTip(const CBlockIndex* pindexNew, con const auto [cur_state, cur_protx_hash] = WITH_READ_LOCK(cs, return std::make_pair(m_state, m_protx_hash)); if (cur_state == MasternodeState::READY) { - auto oldMNList = m_dmnman.GetListForBlock(pindexNew->pprev); - auto newMNList = m_dmnman.GetListForBlock(pindexNew); + CDeterministicMNList oldMNList; + CDeterministicMNList newMNList; + try { + oldMNList = m_dmnman.GetListForBlock(pindexNew->pprev); + newMNList = m_dmnman.GetListForBlock(pindexNew); + } catch (const std::exception& e) { + // GetListForBlock throws when list data is unavailable. This + // callback runs on the scheduler thread, where an uncaught + // exception terminates the node; skip this tip update instead and + // let the next one retry. + LogPrintf("CActiveMasternodeManager::%s -- masternode list unavailable: %s\n", __func__, e.what()); + return; + } auto reset = [this, pindexNew](MasternodeState state) -> void { LOCK(cs); m_state = state; diff --git a/src/bench/load_external.cpp b/src/bench/load_external.cpp index ec97be45ff1e..e11766929430 100644 --- a/src/bench/load_external.cpp +++ b/src/bench/load_external.cpp @@ -48,14 +48,13 @@ static void LoadExternalBlockFile(benchmark::Bench& bench) fclose(file); } - Chainstate& chainstate{testing_setup->m_node.chainman->ActiveChainstate()}; std::multimap blocks_with_unknown_parent; FlatFilePos pos; bench.run([&] { // "rb" is "binary, O_RDONLY", positioned to the start of the file. // The file will be closed by LoadExternalBlockFile(). FILE* file{fsbridge::fopen(blkfile, "rb")}; - chainstate.LoadExternalBlockFile(file, &pos, &blocks_with_unknown_parent); + testing_setup->m_node.chainman->LoadExternalBlockFile(file, &pos, &blocks_with_unknown_parent); }); fs::remove(blkfile); } diff --git a/src/chain.h b/src/chain.h index 9dfb5e86aba6..45d5e9a0ae21 100644 --- a/src/chain.h +++ b/src/chain.h @@ -92,16 +92,20 @@ enum BlockStatus : uint32_t { /** * Only first tx is coinbase, 2 <= coinbase input script length <= 100, transactions valid, no duplicate txids, - * sigops, size, merkle root. Implies all parents are at least TREE but not necessarily TRANSACTIONS. When all - * parent blocks also have TRANSACTIONS, CBlockIndex::nChainTx will be set. + * sigops, size, merkle root. Implies all parents are at least TREE but not necessarily TRANSACTIONS. + * + * If a block's validity is at least VALID_TRANSACTIONS, CBlockIndex::nTx will be set. If a block and all previous + * blocks back to the genesis block or an assumeutxo snapshot block are at least VALID_TRANSACTIONS, + * CBlockIndex::nChainTx will be set. */ BLOCK_VALID_TRANSACTIONS = 3, //! Outputs do not overspend inputs, no double spends, coinbase output ok, no immature coinbase spends, BIP30. - //! Implies all parents are also at least CHAIN. + //! Implies all previous blocks back to the genesis block or an assumeutxo snapshot block are at least VALID_CHAIN. BLOCK_VALID_CHAIN = 4, - //! Scripts & signatures ok. Implies all parents are also at least SCRIPTS. + //! Scripts & signatures ok. Implies all previous blocks back to the genesis block or an assumeutxo snapshot block + //! are at least VALID_SCRIPTS. BLOCK_VALID_SCRIPTS = 5, //! All validity bits. @@ -118,13 +122,8 @@ enum BlockStatus : uint32_t { BLOCK_CONFLICT_CHAINLOCK = 128, //!< conflicts with chainlock system - /** - * If set, this indicates that the block index entry is assumed-valid. - * Certain diagnostics will be skipped in e.g. CheckBlockIndex(). - * It almost certainly means that the block's full validation is pending - * on a background chainstate. See `doc/design/assumeutxo.md`. - */ - BLOCK_ASSUMED_VALID = 256, + BLOCK_STATUS_RESERVED = 256, //!< Unused flag that was previously set on assumeutxo snapshot blocks and their + //!< ancestors before they were validated, and unset when they were validated. }; /** The block chain is a tree shaped structure starting with the @@ -159,21 +158,16 @@ class CBlockIndex //! (memory only) Total amount of work (expected number of hashes) in the chain up to and including this block arith_uint256 nChainWork{}; - //! Number of transactions in this block. + //! Number of transactions in this block. This will be nonzero if the block + //! reached the VALID_TRANSACTIONS level, and zero otherwise. //! Note: in a potential headers-first mode, this number cannot be relied upon - //! Note: this value is faked during UTXO snapshot load to ensure that - //! LoadBlockIndex() will load index entries for blocks that we lack data for. - //! @sa ActivateSnapshot unsigned int nTx{0}; //! (memory only) Number of transactions in the chain up to and including this block. - //! This value will be non-zero only if and only if transactions for this block and all its parents are available. + //! This value will be non-zero if this block and all previous blocks back + //! to the genesis block or an assumeutxo snapshot block have reached the + //! VALID_TRANSACTIONS level. //! Change to 64-bit type before 2024 (assuming worst case of 60 byte transactions). - //! - //! Note: this value is faked during use of a UTXO snapshot because we don't - //! have the underlying block data available during snapshot load. - //! @sa AssumeutxoData - //! @sa ActivateSnapshot unsigned int nChainTx{0}; //! Verification status of this block. See enum BlockStatus @@ -248,13 +242,16 @@ class CBlockIndex } /** - * Check whether this block's and all previous blocks' transactions have been - * downloaded (and stored to disk) at some point. + * Check whether this block and all previous blocks back to the genesis block or an assumeutxo snapshot block have + * reached VALID_TRANSACTIONS and had transactions downloaded (and stored to disk) at some point. * * 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, 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 { @@ -300,14 +297,6 @@ class CBlockIndex return ((nStatus & BLOCK_VALID_MASK) >= nUpTo); } - //! @returns true if the block is assumed-valid; this means it is queued to be - //! validated by a background chainstate. - bool IsAssumedValid() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main) - { - AssertLockHeld(::cs_main); - return nStatus & BLOCK_ASSUMED_VALID; - } - //! Raise the validity level of this block index entry. //! Returns true if the validity was changed. bool RaiseValidity(enum BlockStatus nUpTo) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) @@ -317,12 +306,6 @@ class CBlockIndex if (nStatus & BLOCK_FAILED_MASK) return false; if ((nStatus & BLOCK_VALID_MASK) < nUpTo) { - // If this block had been marked assumed-valid and we're raising - // its validity to a certain point, there is no longer an assumption. - if (nStatus & BLOCK_ASSUMED_VALID && nUpTo >= BLOCK_VALID_SCRIPTS) { - nStatus &= ~BLOCK_ASSUMED_VALID; - } - nStatus = (nStatus & ~BLOCK_VALID_MASK) | nUpTo; return true; } diff --git a/src/chainlock/handler.cpp b/src/chainlock/handler.cpp index c20ef531bf53..8d5f88ceda4e 100644 --- a/src/chainlock/handler.cpp +++ b/src/chainlock/handler.cpp @@ -216,8 +216,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 8d50dd2867dc..4a849ee5bf95 100644 --- a/src/chainlock/handler.h +++ b/src/chainlock/handler.h @@ -112,7 +112,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 b9e2f04c5fb2..e89cf2453a05 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 8249ceb9a79a..50c498ee68c3 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 3d391b691e52..ec79c18bbe2a 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")}, 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")}, 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("0xf6571ed786c40dcbb835b38090eaca87762cf421874461caa779738c7ff602fa")}, + .evo_hash = EvoSnapshotHash{uint256S("0xc3cc873878e8d1714ac14149eaae0ccf88b10f2a691ca9256fb4326ff5ec4001")}, + .nChainTx = 334, + .blockhash = uint256S("0x2a9b2c0ea78a47f289053bbb206314e3871b4d4fbf8b6a5f558ca2e8382c57f2"), }, }; + 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, @@ -1360,6 +1405,16 @@ void CDevNetParams::UpdateLLMQDevnetParametersFromArgs(const ArgsManager& args) UpdateLLMQDevnetParameters(size, threshold); } +std::vector CChainParams::GetAvailableSnapshotHeights() const +{ + std::vector heights; + heights.reserve(m_assumeutxo_data.size()); + for (const auto& data : m_assumeutxo_data) { + heights.emplace_back(data.height); + } + return heights; +} + static std::unique_ptr globalChainParams; const CChainParams &Params() { @@ -1392,6 +1447,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 69b4baaa61fa..2f4105b51dc4 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -34,23 +35,35 @@ struct AssumeutxoHash : public BaseHash { explicit AssumeutxoHash(const uint256& hash) : BaseHash(hash) {} }; +struct EvoSnapshotHash : public BaseHash { + explicit EvoSnapshotHash(const uint256& hash) : BaseHash(hash) {} +}; + /** * Holds configuration for use during UTXO snapshot load and validation. The contents * here are security critical, since they dictate which UTXO snapshots are recognized * as valid. */ struct AssumeutxoData { + int height; + //! The expected hash of the deserialized UTXO set. - const AssumeutxoHash hash_serialized; + AssumeutxoHash hash_serialized; - //! Used to populate the nChainTx value, which is used during BlockManager::LoadBlockIndex(). + //! The expected single-SHA256 hash of the canonical Dash evo section. + EvoSnapshotHash evo_hash; + + //! Used to populate the snapshot base block's nChainTx value during snapshot activation and + //! BlockManager::LoadBlockIndex(). Counts for earlier blocks remain unset until their data is received. //! //! 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 @@ -130,9 +143,15 @@ 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; }); + } + std::vector GetAvailableSnapshotHeights() const; const ChainTxData& TxData() const { return chainTxData; } void UpdateDIP3Parameters(int nActivationHeight, int nEnforcementHeight); @@ -179,7 +198,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/dbwrapper.h b/src/dbwrapper.h index 4060d8138d99..95ef9fb9dca5 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -669,6 +669,22 @@ class CDBTransaction { return parent.Read(ssKey, value); } + /** Read a value only if it is present in this transaction's write set. */ + template + bool ReadPending(const K& key, V& value) { + const CDataStream ssKey = KeyToDataStream(key); + auto it = writes.find(ssKey); + if (it == writes.end()) { + return false; + } + auto* impl = dynamic_cast*>(it->second.get()); + if (!impl) { + throw std::runtime_error("ReadPending called with V != previously written type"); + } + value = impl->value; + return true; + } + template bool Exists(const K& key) { return Exists(KeyToDataStream(key)); diff --git a/src/dsnotificationinterface.cpp b/src/dsnotificationinterface.cpp index 4ebc9ccb4c34..b80f74c7c765 100644 --- a/src/dsnotificationinterface.cpp +++ b/src/dsnotificationinterface.cpp @@ -78,8 +78,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 889438397fc5..4ab61d486e0b 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/evo/assetlocktx.cpp b/src/evo/assetlocktx.cpp index cba0bdc55534..966845f77ce9 100644 --- a/src/evo/assetlocktx.cpp +++ b/src/evo/assetlocktx.cpp @@ -96,7 +96,10 @@ std::string CAssetLockPayload::ToString() const const std::string ASSETUNLOCK_REQUESTID_PREFIX = "plwdtx"; -bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const uint256& msgHash, gsl::not_null pindexTip, TxValidationState& state) const +template +static bool VerifyAssetUnlockSig(const CAssetUnlockPayload& payload, ScanQuorums&& scan_quorums, + GetQuorum&& get_quorum, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) { // That quourm hash must be active at `requestHeight`, // and at the quorumHash must be active in either the current or previous quorum cycle @@ -110,36 +113,60 @@ bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const uint // We check all active quorums + 1 the latest inactive const int quorums_to_scan = llmq_params_opt->signingActiveQuorumCount + 1; - const auto quorums = qman.ScanQuorums(llmqType, pindexTip, quorums_to_scan); + const auto quorums = scan_quorums(llmqType, pindexTip, quorums_to_scan); - if (bool isActive = std::any_of(quorums.begin(), quorums.end(), [&](const auto &q) { return q->qc->quorumHash == quorumHash; }); !isActive) { + if (bool isActive = std::any_of(quorums.begin(), quorums.end(), [&](const auto &q) { return q->qc->quorumHash == payload.getQuorumHash(); }); !isActive) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-too-old-quorum"); } - if (static_cast(pindexTip->nHeight) < requestedHeight || pindexTip->nHeight >= getHeightToExpiry()) { + if (static_cast(pindexTip->nHeight) < payload.getRequestedHeight() || pindexTip->nHeight >= payload.getHeightToExpiry()) { LogPrint(BCLog::CREDITPOOL, "Asset unlock tx %d with requested height %d could not be accepted on height: %d\n", - index, requestedHeight, pindexTip->nHeight); + payload.getIndex(), payload.getRequestedHeight(), pindexTip->nHeight); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-too-late"); } - const auto quorum = qman.GetQuorum(llmqType, quorumHash); + const auto quorum = get_quorum(llmqType, payload.getQuorumHash()); // quorum must be valid at this point. Let's check and throw error just in case if (!quorum) { - LogPrintf("%s: ERROR! No quorum for credit pool found for hash=%s\n", __func__, quorumHash.ToString()); + LogPrintf("%s: ERROR! No quorum for credit pool found for hash=%s\n", __func__, payload.getQuorumHash().ToString()); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-quorum-internal-error"); } - const uint256 requestId = ::SerializeHash(std::make_pair(ASSETUNLOCK_REQUESTID_PREFIX, index)); + const uint256 requestId = ::SerializeHash(std::make_pair(ASSETUNLOCK_REQUESTID_PREFIX, payload.getIndex())); if (const llmq::SignHash signHash(llmqType, quorum->qc->quorumHash, requestId, msgHash); - quorumSig.VerifyInsecure(quorum->qc->quorumPublicKey, signHash.Get())) { + payload.getQuorumSig().VerifyInsecure(quorum->qc->quorumPublicKey, signHash.Get())) { return true; } return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-not-verified"); } -bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null pindexPrev, const std::optional& indexes, TxValidationState& state) +bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) const +{ + return VerifyAssetUnlockSig(*this, [&](Consensus::LLMQType llmq_type, const CBlockIndex* pindex, size_t count) { + return qman.ScanQuorums(llmq_type, pindex, count); + }, [&](Consensus::LLMQType llmq_type, const uint256& quorum_hash) { + return qman.GetQuorum(llmq_type, quorum_hash); + }, msgHash, pindexTip, state); +} + +bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const CChain& chain, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) const +{ + AssertLockHeld(::cs_main); + return VerifyAssetUnlockSig(*this, [&](Consensus::LLMQType llmq_type, const CBlockIndex* pindex, size_t count) NO_THREAD_SAFETY_ANALYSIS { + return qman.ScanQuorums(llmq_type, pindex, count, chain); + }, [&](Consensus::LLMQType llmq_type, const uint256& quorum_hash) NO_THREAD_SAFETY_ANALYSIS { + return qman.GetQuorum(llmq_type, quorum_hash, chain); + }, msgHash, pindexTip, state); +} + +template +static bool CheckAssetUnlockTxImpl(const BlockManager& blockman, VerifySig&& verify_sig, const CTransaction& tx, + gsl::not_null pindexPrev, + const std::optional& indexes, TxValidationState& state) { // Some checks depends from blockchain status also, such as `known indexes` and `withdrawal limits` // They are omitted here and done by CCreditPool @@ -180,7 +207,28 @@ bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager uint256 msgHash = tx_copy.GetHash(); - return assetUnlockTx.VerifySig(qman, msgHash, pindexPrev, state); + return verify_sig(assetUnlockTx, msgHash, pindexPrev, state); +} + +bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, + gsl::not_null pindexPrev, const std::optional& indexes, + TxValidationState& state) +{ + return CheckAssetUnlockTxImpl(blockman, [&](const CAssetUnlockPayload& payload, const uint256& msg_hash, + const CBlockIndex* pindex, TxValidationState& tx_state) { + return payload.VerifySig(qman, msg_hash, pindex, tx_state); + }, tx, pindexPrev, indexes, state); +} + +bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CChain& chain, + const CTransaction& tx, gsl::not_null pindexPrev, + const std::optional& indexes, TxValidationState& state) +{ + AssertLockHeld(::cs_main); + return CheckAssetUnlockTxImpl(blockman, [&](const CAssetUnlockPayload& payload, const uint256& msg_hash, + const CBlockIndex* pindex, TxValidationState& tx_state) NO_THREAD_SAFETY_ANALYSIS { + return payload.VerifySig(qman, chain, msg_hash, pindex, tx_state); + }, tx, pindexPrev, indexes, state); } bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state) diff --git a/src/evo/assetlocktx.h b/src/evo/assetlocktx.h index 6a00f605f2c0..634174e1b0f6 100644 --- a/src/evo/assetlocktx.h +++ b/src/evo/assetlocktx.h @@ -10,13 +10,17 @@ #include #include #include +#include +#include #include #include class CBlockIndex; +class CChain; class CRangesSet; class TxValidationState; +extern RecursiveMutex cs_main; // NOLINT(readability-redundant-declaration) struct RPCResult; namespace llmq { class CQuorumManager; @@ -114,6 +118,9 @@ class CAssetUnlockPayload [[nodiscard]] UniValue ToJson() const; bool VerifySig(const llmq::CQuorumManager& qman, const uint256& msgHash, gsl::not_null pindexTip, TxValidationState& state) const; + bool VerifySig(const llmq::CQuorumManager& qman, const CChain& chain, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) const + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); // getters uint8_t getVersion() const @@ -156,6 +163,10 @@ class CAssetUnlockPayload bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state); bool CheckAssetUnlockTx(const node::BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null pindexPrev, const std::optional& indexes, TxValidationState& state); +bool CheckAssetUnlockTx(const node::BlockManager& blockman, const llmq::CQuorumManager& qman, const CChain& chain, + const CTransaction& tx, gsl::not_null pindexPrev, + const std::optional& indexes, TxValidationState& state) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state); #endif // BITCOIN_EVO_ASSETLOCKTX_H diff --git a/src/evo/chainhelper.cpp b/src/evo/chainhelper.cpp index 06a610e92194..6af84ff98d9d 100644 --- a/src/evo/chainhelper.cpp +++ b/src/evo/chainhelper.cpp @@ -7,14 +7,18 @@ #include #include #include +#include #include +#include #include #include +#include #include #include #include #include #include +#include CChainstateHelper::CChainstateHelper(CEvoDB& evodb, CDeterministicMNManager& dmnman, const CMasternodeSync& mn_sync, llmq::CInstantSendManager& isman, llmq::CQuorumBlockProcessor& qblockman, @@ -23,6 +27,9 @@ CChainstateHelper::CChainstateHelper(CEvoDB& evodb, CDeterministicMNManager& dmn const llmq::CQuorumManager& qman) : isman{isman}, mn_sync{mn_sync}, + m_dmnman{dmnman}, + m_qblockman{qblockman}, + m_qsnapman{qsnapman}, credit_pool_manager{std::make_unique(evodb, chainman)}, m_chainlocks{chainlocks}, ehf_manager{std::make_unique(evodb, chainman)}, @@ -60,6 +67,11 @@ bool CChainstateHelper::HasChainLock(int nHeight, const uint256& blockHash) cons int32_t CChainstateHelper::GetBestChainLockHeight() const { return m_chainlocks.GetBestChainLockHeight(); } +uint256 CChainstateHelper::GetDeterministicMNListHash(const CBlockIndex* pindex) const +{ + return evo::CanonicalMNListHash(m_dmnman.GetListForBlock(Assert(pindex))); +} + /** Passthrough functions to CCreditPoolManager */ CCreditPool CChainstateHelper::GetCreditPool(const CBlockIndex* const pindex) { @@ -85,7 +97,7 @@ bool CChainstateHelper::RemoveConflictingISLockByTx(const CTransaction& tx) return true; } -std::unordered_map CChainstateHelper::GetSignalsStage(const CBlockIndex* const pindexPrev) +std::map CChainstateHelper::GetSignalsStage(const CBlockIndex* const pindexPrev) { return ehf_manager->GetSignalsStage(pindexPrev); } diff --git a/src/evo/chainhelper.h b/src/evo/chainhelper.h index f3d0cbc2c34c..2c5c6940de1d 100644 --- a/src/evo/chainhelper.h +++ b/src/evo/chainhelper.h @@ -6,9 +6,9 @@ #define BITCOIN_EVO_CHAINHELPER_H #include +#include #include #include -#include class CBlockIndex; class CCreditPoolManager; @@ -42,6 +42,9 @@ class CChainstateHelper private: llmq::CInstantSendManager& isman; const CMasternodeSync& mn_sync; + CDeterministicMNManager& m_dmnman; + llmq::CQuorumBlockProcessor& m_qblockman; + llmq::CQuorumSnapshotManager& m_qsnapman; public: const std::unique_ptr credit_pool_manager; @@ -69,6 +72,12 @@ class CChainstateHelper bool HasChainLock(int nHeight, const uint256& blockHash) const; int32_t GetBestChainLockHeight() const; + /** Return a canonical hash of the deterministic MN list derived at a block. */ + uint256 GetDeterministicMNListHash(const CBlockIndex* pindex) const; + CDeterministicMNManager& DeterministicMNManager() { return m_dmnman; } + llmq::CQuorumBlockProcessor& QuorumBlockProcessor() { return m_qblockman; } + llmq::CQuorumSnapshotManager& QuorumSnapshotManager() { return m_qsnapman; } + /** Passthrough functions to CCreditPoolManager */ CCreditPool GetCreditPool(const CBlockIndex* const pindex); @@ -77,7 +86,7 @@ class CChainstateHelper bool IsInstantSendWaitingForTx(const uint256& hash) const; bool RemoveConflictingISLockByTx(const CTransaction& tx); - std::unordered_map GetSignalsStage(const CBlockIndex* const pindexPrev); + std::map GetSignalsStage(const CBlockIndex* const pindexPrev); }; #endif // BITCOIN_EVO_CHAINHELPER_H diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index 980891fd7c95..85dca6008208 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -124,25 +125,40 @@ std::optional CCreditPoolManager::GetFromCache(const CBlockIndex& b return pool; } } - if (block_index.nHeight % DISK_SNAPSHOT_PERIOD == 0) { - if (evoDb.Read(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool)) { - LOCK(cache_mutex); - creditPoolCache.insert(block_hash, pool); - return pool; - } + // Snapshot activation may deliberately seed a full state at a height that + // is not one of the normal periodic checkpoints. + if (evoDb.Read(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool)) { + LOCK(cache_mutex); + creditPoolCache.insert(block_hash, pool); + return pool; } return std::nullopt; } void CCreditPoolManager::AddToCache(const uint256& block_hash, int height, const CCreditPool &pool) { + if (height % DISK_SNAPSHOT_PERIOD == 0) { + if (!evoDb.WriteDerived(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool)) { + // A mismatch is local EvoDB corruption, not a statement about the + // block. Abort here: some callers (miner, RPC) never pass through a + // validation-state catch, and the block-connect catches must not + // translate this into a consensus rejection. + const std::string msg = strprintf("CCreditPoolManager::%s -- EvoDB credit pool mismatch for block %s", + __func__, block_hash.ToString()); + AbortNode(msg); + throw EvoDbInconsistencyError(msg); + } + } { LOCK(cache_mutex); creditPoolCache.insert(block_hash, pool); } - if (height % DISK_SNAPSHOT_PERIOD == 0) { - evoDb.Write(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool); - } +} + +bool CCreditPoolManager::SeedSnapshot(const CBlockIndex* block, const CCreditPool& pool) +{ + assert(block != nullptr); + return evoDb.WriteDerived(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block->GetBlockHash()), pool); } CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null block_index, CCreditPool prev) @@ -331,6 +347,11 @@ std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpo } } return creditPoolDiff; + } catch (const EvoDbInconsistencyError& e) { + // Local EvoDB corruption (the node is already aborting): fail with + // M_ERROR so the block is not marked invalid. + state.Error(e.what()); + return std::nullopt; } catch (const std::exception& e) { LogPrintf("%s -- failed: %s\n", __func__, e.what()); state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-getcreditpooldiff"); diff --git a/src/evo/creditpool.h b/src/evo/creditpool.h index fec44ab680cb..196632bb7290 100644 --- a/src/evo/creditpool.h +++ b/src/evo/creditpool.h @@ -134,6 +134,8 @@ class CCreditPoolManager * it can happen if there limits of withdrawal (unlock) exceed */ CCreditPool GetCreditPool(const CBlockIndex* block) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); + /** Seed a full pool snapshot in the current EvoDB transaction. */ + bool SeedSnapshot(const CBlockIndex* block, const CCreditPool& pool) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); private: std::optional GetFromCache(const CBlockIndex& block_index) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 20db7a1f08ec..dedb97940ac2 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -17,6 +17,7 @@ #include #include #include