diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 5e9d47f8c4c7..b16f02f9aae3 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -110,6 +110,7 @@ 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 \ 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..7565c013e2a9 100644 --- a/src/active/masternode.cpp +++ b/src/active/masternode.cpp @@ -178,8 +178,21 @@ 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 BlockDataUnavailableError& e) { + // This callback runs on the scheduler thread, where an uncaught + // exception terminates the node. Unavailable history is expected + // while a snapshot's background chainstate is still catching up, so + // skip this tip update and let the next one retry. Any other + // exception means local EvoDB/list corruption and must not be + // hidden, so it deliberately stays unhandled. + 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/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/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..acffa3cd2d4a 100644 --- a/src/evo/chainhelper.cpp +++ b/src/evo/chainhelper.cpp @@ -85,7 +85,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..f68c48bd26bf 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; @@ -77,7 +77,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..5d9a1f60d3cf 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -136,13 +137,22 @@ std::optional CCreditPoolManager::GetFromCache(const CBlockIndex& b 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); - } } CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null block_index, CCreditPool prev) @@ -331,6 +341,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/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 20db7a1f08ec..b25f4d01a514 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -17,6 +17,7 @@ #include #include #include