LCOV - code coverage report
Current view: top level - src/evo - deterministicmns.cpp (source / functions) Hit Total Coverage
Test: test_dash_coverage.info Lines: 361 881 41.0 %
Date: 2026-06-25 07:23:51 Functions: 40 63 63.5 %

          Line data    Source code
       1             : // Copyright (c) 2018-2025 The Dash Core developers
       2             : // Distributed under the MIT software license, see the accompanying
       3             : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
       4             : 
       5             : #include <evo/deterministicmns.h>
       6             : 
       7             : #include <chainparams.h>
       8             : #include <coins.h>
       9             : #include <consensus/validation.h>
      10             : #include <deploymentstatus.h>
      11             : #include <evo/dmn_types.h>
      12             : #include <evo/dmnstate.h>
      13             : #include <evo/evodb.h>
      14             : #include <evo/providertx.h>
      15             : #include <evo/simplifiedmns.h>
      16             : #include <evo/specialtx.h>
      17             : #include <masternode/meta.h>
      18             : #include <node/blockstorage.h>
      19             : #include <script/standard.h>
      20             : #include <stats/client.h>
      21             : #include <uint256.h>
      22             : #include <util/helpers.h>
      23             : 
      24             : #include <univalue.h>
      25             : 
      26             : #include <functional>
      27             : #include <optional>
      28             : #include <memory>
      29             : #include <ranges>
      30             : 
      31             : static const std::string DB_LIST_SNAPSHOT = "dmn_S3";
      32             : static const std::string DB_LIST_DIFF = "dmn_D4";        // Bumped for nVersion-first format
      33             : static const std::string DB_LIST_DIFF_LEGACY = "dmn_D3"; // Legacy format key
      34             : static const std::string DB_LIST_REPAIRED = "dmn_R1";
      35             : 
      36        3882 : uint64_t CDeterministicMN::GetInternalId() const
      37             : {
      38             :     // can't get it if it wasn't set yet
      39        3882 :     assert(internalId != std::numeric_limits<uint64_t>::max());
      40        3882 :     return internalId;
      41             : }
      42             : 
      43       17159 : CSimplifiedMNListEntry CDeterministicMN::to_sml_entry() const
      44             : {
      45       17159 :     const CDeterministicMNState& state{*pdmnState};
      46       34318 :     return CSimplifiedMNListEntry(proTxHash, state.confirmedHash, state.netInfo, state.pubKeyOperator,
      47       17159 :                                   state.keyIDVoting, !state.IsBanned(), state.platformHTTPPort, state.platformNodeID,
      48       17159 :                                   state.scriptPayout, state.scriptOperatorPayout, state.nVersion, nType);
      49             : }
      50             : 
      51           0 : std::string CDeterministicMN::ToString() const
      52             : {
      53           0 :     return strprintf("CDeterministicMN(proTxHash=%s, collateralOutpoint=%s, nOperatorReward=%f, state=%s", proTxHash.ToString(), collateralOutpoint.ToStringShort(), (double)nOperatorReward / 100, pdmnState->ToString());
      54           0 : }
      55             : 
      56           0 : bool CDeterministicMNList::IsMNValid(const uint256& proTxHash) const
      57             : {
      58           0 :     auto p = mnMap.find(proTxHash);
      59           0 :     if (p == nullptr) {
      60           0 :         return false;
      61             :     }
      62           0 :     return !(*p)->pdmnState->IsBanned();
      63           0 : }
      64             : 
      65         916 : bool CDeterministicMNList::IsMNPoSeBanned(const uint256& proTxHash) const
      66             : {
      67         916 :     auto p = mnMap.find(proTxHash);
      68         916 :     if (p == nullptr) {
      69           0 :         return false;
      70             :     }
      71         916 :     return (*p)->pdmnState->IsBanned();
      72         916 : }
      73             : 
      74       13348 : CDeterministicMNCPtr CDeterministicMNList::GetMN(const uint256& proTxHash) const
      75             : {
      76       13348 :     auto p = mnMap.find(proTxHash);
      77       13348 :     if (p == nullptr) {
      78         285 :         return nullptr;
      79             :     }
      80       13063 :     return *p;
      81       13348 : }
      82             : 
      83           0 : CDeterministicMNCPtr CDeterministicMNList::GetValidMN(const uint256& proTxHash) const
      84             : {
      85           0 :     auto dmn = GetMN(proTxHash);
      86           0 :     if (dmn && dmn->pdmnState->IsBanned()) {
      87           0 :         return nullptr;
      88             :     }
      89           0 :     return dmn;
      90           0 : }
      91             : 
      92           0 : CDeterministicMNCPtr CDeterministicMNList::GetMNByOperatorKey(const CBLSPublicKey& pubKey) const
      93             : {
      94           0 :     const auto it = std::ranges::find_if(mnMap, [&pubKey](const auto& p) {
      95           0 :         return p.second->pdmnState->pubKeyOperator.Get() == pubKey;
      96             :     });
      97           0 :     if (it == mnMap.end()) {
      98           0 :         return nullptr;
      99             :     }
     100           0 :     return it->second;
     101           0 : }
     102             : 
     103       25894 : CDeterministicMNCPtr CDeterministicMNList::GetMNByCollateral(const COutPoint& collateralOutpoint) const
     104             : {
     105       25894 :     return GetUniquePropertyMN(collateralOutpoint);
     106             : }
     107             : 
     108           0 : CDeterministicMNCPtr CDeterministicMNList::GetValidMNByCollateral(const COutPoint& collateralOutpoint) const
     109             : {
     110           0 :     auto dmn = GetMNByCollateral(collateralOutpoint);
     111           0 :     if (dmn && dmn->pdmnState->IsBanned()) {
     112           0 :         return nullptr;
     113             :     }
     114           0 :     return dmn;
     115           0 : }
     116             : 
     117           0 : CDeterministicMNCPtr CDeterministicMNList::GetMNByService(const CService& service) const
     118             : {
     119           0 :     return GetUniquePropertyMN(service);
     120             : }
     121             : 
     122           9 : CDeterministicMNCPtr CDeterministicMNList::GetMNByInternalId(uint64_t internalId) const
     123             : {
     124           9 :     auto proTxHash = mnInternalIdMap.find(internalId);
     125           9 :     if (!proTxHash) {
     126           0 :         return nullptr;
     127             :     }
     128           9 :     return GetMN(*proTxHash);
     129           9 : }
     130             : 
     131       23044 : static int CompareByLastPaid_GetHeight(const CDeterministicMN& dmn)
     132             : {
     133       23044 :     int height = dmn.pdmnState->nLastPaidHeight;
     134       23044 :     if (dmn.pdmnState->nPoSeRevivedHeight != -1 && dmn.pdmnState->nPoSeRevivedHeight > height) {
     135         432 :         height = dmn.pdmnState->nPoSeRevivedHeight;
     136       23044 :     } else if (height == 0) {
     137        3619 :         height = dmn.pdmnState->nRegisteredHeight;
     138        3619 :     }
     139       23044 :     return height;
     140             : }
     141             : 
     142       11522 : static bool CompareByLastPaid(const CDeterministicMN& _a, const CDeterministicMN& _b)
     143             : {
     144       11522 :     int ah = CompareByLastPaid_GetHeight(_a);
     145       11522 :     int bh = CompareByLastPaid_GetHeight(_b);
     146       11522 :     if (ah == bh) {
     147         541 :         return _a.proTxHash < _b.proTxHash;
     148             :     } else {
     149       10981 :         return ah < bh;
     150             :     }
     151       11522 : }
     152       11522 : static bool CompareByLastPaid(const CDeterministicMN* _a, const CDeterministicMN* _b)
     153             : {
     154       11522 :     return CompareByLastPaid(*_a, *_b);
     155             : }
     156             : 
     157       42745 : CDeterministicMNCPtr CDeterministicMNList::GetMNPayee(gsl::not_null<const CBlockIndex*> pindexPrev) const
     158             : {
     159       42745 :     if (mnMap.size() == 0) {
     160       28289 :         return nullptr;
     161             :     }
     162             : 
     163             :     // The flag is-v19-activate is used for optimization; we don't need to go over all masternodes every pre-v19 block
     164       14456 :     const bool isv19Active{DeploymentActiveAfter(pindexPrev, Params().GetConsensus(), Consensus::DEPLOYMENT_V19)};
     165       14456 :     const bool isMNRewardReallocation{DeploymentActiveAfter(pindexPrev, Params().GetConsensus(), Consensus::DEPLOYMENT_MN_RR)};
     166             :     // EvoNodes are rewarded 4 blocks in a row until MNRewardReallocation (Platform release)
     167             :     // For optimization purposes we also check if v19 active to avoid loop over all masternodes
     168       14456 :     CDeterministicMNCPtr best = nullptr;
     169       14456 :     if (isv19Active && !isMNRewardReallocation) {
     170       24318 :         ForEachMNShared(/*onlyValid=*/true, [&](const auto& dmn) {
     171       12159 :             if (dmn->pdmnState->nLastPaidHeight == nHeight) {
     172             :                 // We found the last MN Payee.
     173             :                 // If the last payee is an EvoNode, we need to check its consecutive payments and pay him again if needed
     174       12152 :                 if (dmn->nType == MnType::Evo && dmn->pdmnState->nConsecutivePayments < dmn_types::Evo.voting_weight) {
     175           0 :                     best = dmn;
     176           0 :                 }
     177       12152 :             }
     178       12159 :         });
     179             : 
     180       12159 :         if (best != nullptr) return best;
     181             : 
     182             :         // Note: If the last payee was a regular MN or if the payee is an EvoNode that was removed from the mnList then that's fine.
     183             :         // We can proceed with classic MN payee selection
     184       12159 :     }
     185             : 
     186       40429 :     ForEachMNShared(/*onlyValid=*/true, [&](const auto& dmn) {
     187       25973 :         if (best == nullptr || CompareByLastPaid(dmn.get(), best.get())) {
     188       16557 :             best = dmn;
     189       16557 :         }
     190       25973 :     });
     191             : 
     192       14456 :     return best;
     193       57201 : }
     194             : 
     195           0 : std::vector<CDeterministicMNCPtr> CDeterministicMNList::GetProjectedMNPayees(gsl::not_null<const CBlockIndex* const> pindexPrev, int nCount) const
     196             : {
     197           0 :     if (nCount < 0 ) {
     198           0 :         return {};
     199             :     }
     200           0 :     const bool isMNRewardReallocation = DeploymentActiveAfter(pindexPrev, Params().GetConsensus(),
     201             :                                                               Consensus::DEPLOYMENT_MN_RR);
     202           0 :     const auto counts = GetCounts();
     203           0 :     const auto weighted_count = isMNRewardReallocation ? counts.enabled() : counts.m_valid_weighted;
     204           0 :     nCount = std::min(nCount, int(weighted_count));
     205             : 
     206           0 :     std::vector<CDeterministicMNCPtr> result;
     207           0 :     result.reserve(weighted_count);
     208             : 
     209           0 :     int remaining_evo_payments{0};
     210           0 :     CDeterministicMNCPtr evo_to_be_skipped{nullptr};
     211           0 :     if (!isMNRewardReallocation) {
     212           0 :         ForEachMNShared(/*onlyValid=*/true, [&](const auto& dmn) {
     213           0 :             if (dmn->pdmnState->nLastPaidHeight == nHeight) {
     214             :                 // We found the last MN Payee.
     215             :                 // If the last payee is an EvoNode, we need to check its consecutive payments and pay him again if needed
     216           0 :                 if (dmn->nType == MnType::Evo && dmn->pdmnState->nConsecutivePayments < dmn_types::Evo.voting_weight) {
     217           0 :                     remaining_evo_payments = dmn_types::Evo.voting_weight - dmn->pdmnState->nConsecutivePayments;
     218           0 :                     for ([[maybe_unused]] auto _ : util::irange(remaining_evo_payments)) {
     219           0 :                         result.emplace_back(dmn);
     220           0 :                         evo_to_be_skipped = dmn;
     221             :                     }
     222           0 :                 }
     223           0 :             }
     224           0 :         });
     225           0 :     }
     226             : 
     227           0 :     ForEachMNShared(/*onlyValid=*/true, [&](const auto& dmn) {
     228           0 :         if (dmn == evo_to_be_skipped) return;
     229           0 :         for ([[maybe_unused]] auto _ : util::irange(isMNRewardReallocation ? 1 : GetMnType(dmn->nType).voting_weight)) {
     230           0 :             result.emplace_back(dmn);
     231             :         }
     232           0 :     });
     233             : 
     234           0 :     if (evo_to_be_skipped != nullptr) {
     235             :         // if EvoNode is in the middle of payments, add entries for already paid ones to the end of the list
     236           0 :         for ([[maybe_unused]] auto _ : util::irange(evo_to_be_skipped->pdmnState->nConsecutivePayments)) {
     237           0 :             result.emplace_back(evo_to_be_skipped);
     238             :         }
     239           0 :     }
     240             : 
     241           0 :     std::sort(result.begin() + remaining_evo_payments, result.end(), [&](const CDeterministicMNCPtr& a, const CDeterministicMNCPtr& b) {
     242           0 :         return CompareByLastPaid(a.get(), b.get());
     243             :     });
     244             : 
     245           0 :     result.resize(nCount);
     246             : 
     247           0 :     return result;
     248           0 : }
     249             : 
     250       45575 : gsl::not_null<std::shared_ptr<const CSimplifiedMNList>> CDeterministicMNList::to_sml() const
     251             : {
     252       45575 :     LOCK(m_cached_sml_mutex);
     253       45575 :     if (!m_cached_sml) {
     254         208 :         std::vector<std::unique_ptr<CSimplifiedMNListEntry>> sml_entries;
     255         208 :         sml_entries.reserve(mnMap.size());
     256             : 
     257        1019 :         ForEachMN(/*onlyValid=*/false, [&sml_entries](const auto& dmn) {
     258         811 :             sml_entries.emplace_back(std::make_unique<CSimplifiedMNListEntry>(dmn.to_sml_entry()));
     259         811 :         });
     260         208 :         m_cached_sml = std::make_shared<CSimplifiedMNList>(std::move(sml_entries));
     261         208 :     }
     262             : 
     263       45575 :     return m_cached_sml;
     264       45575 : }
     265             : 
     266           0 : int CDeterministicMNList::CalcMaxPoSePenalty() const
     267             : {
     268             :     // Maximum PoSe penalty is dynamic and equals the number of registered MNs
     269             :     // It's however at least 100.
     270             :     // This means that the max penalty is usually equal to a full payment cycle
     271           0 :     return std::max(100, (int)GetCounts().total());
     272             : }
     273             : 
     274           0 : int CDeterministicMNList::CalcPenalty(int percent) const
     275             : {
     276           0 :     assert(percent > 0);
     277           0 :     return (CalcMaxPoSePenalty() * percent) / 100;
     278             : }
     279             : 
     280           0 : void CDeterministicMNList::PoSePunish(const uint256& proTxHash, int penalty, bool debugLogs)
     281             : {
     282           0 :     assert(penalty > 0);
     283             : 
     284           0 :     auto dmn = GetMN(proTxHash);
     285           0 :     if (!dmn) {
     286           0 :         throw(std::runtime_error(strprintf("%s: Can't find a masternode with proTxHash=%s", __func__, proTxHash.ToString())));
     287             :     }
     288             : 
     289           0 :     int maxPenalty = CalcMaxPoSePenalty();
     290             : 
     291           0 :     auto newState = std::make_shared<CDeterministicMNState>(*dmn->pdmnState);
     292           0 :     newState->nPoSePenalty += penalty;
     293           0 :     newState->nPoSePenalty = std::min(maxPenalty, newState->nPoSePenalty);
     294             : 
     295           0 :     if (!dmn->pdmnState->IsBanned()) {
     296           0 :         if (newState->nPoSePenalty >= maxPenalty) {
     297           0 :             newState->BanIfNotBanned(nHeight);
     298           0 :         }
     299           0 :         if (debugLogs) {
     300           0 :             LogPrintf("CDeterministicMNList::%s -- %s MN %s at height %d, penalty %d->%d (max=%d)\n", __func__,
     301             :                       newState->IsBanned() ? "banned" : "punished", proTxHash.ToString(), nHeight,
     302             :                       dmn->pdmnState->nPoSePenalty, newState->nPoSePenalty, maxPenalty);
     303           0 :         }
     304           0 :     }
     305           0 :     UpdateMN(proTxHash, newState);
     306           0 : }
     307             : 
     308       36470 : void CDeterministicMNList::DecreaseScores()
     309             : {
     310       36470 :     std::vector<CDeterministicMNCPtr> toDecrease;
     311       36470 :     toDecrease.reserve(GetCounts().total() / 10);
     312             :     // only iterate and decrease for valid ones (not PoSe banned yet)
     313             :     // if a MN ever reaches the maximum, it stays in PoSe banned state until revived
     314       50511 :     ForEachMNShared(/*onlyValid=*/true, [&toDecrease](const auto& dmn) {
     315             :         // There is no reason to check if this MN is banned here since onlyValid=true will only run on non-banned MNs
     316       14041 :         if (dmn->pdmnState->nPoSePenalty > 0) {
     317           0 :             toDecrease.emplace_back(dmn);
     318           0 :         }
     319       14041 :     });
     320             : 
     321       36470 :     for (const auto& proTxHash : toDecrease) {
     322           0 :         PoSeDecrease(*proTxHash);
     323             :     }
     324       36470 : }
     325             : 
     326           0 : void CDeterministicMNList::PoSeDecrease(const CDeterministicMN& dmn)
     327             : {
     328           0 :     assert(dmn.pdmnState->nPoSePenalty > 0 && !dmn.pdmnState->IsBanned());
     329             : 
     330           0 :     auto newState = std::make_shared<CDeterministicMNState>(*dmn.pdmnState);
     331           0 :     newState->nPoSePenalty--;
     332           0 :     UpdateMN(dmn, newState);
     333           0 : }
     334             : 
     335        9102 : CDeterministicMNListDiff CDeterministicMNList::BuildDiff(const CDeterministicMNList& to) const
     336             : {
     337        9102 :     CDeterministicMNListDiff diffRet;
     338             : 
     339       12656 :     for (const auto& p : to.mnMap) {
     340        3554 :         const auto& toPtr = p.second;
     341        3554 :         auto fromPtr = GetMN(toPtr->proTxHash);
     342        3554 :         if (fromPtr == nullptr) {
     343          36 :             diffRet.addedMNs.emplace_back(toPtr);
     344        3554 :         } else if (fromPtr != toPtr || fromPtr->pdmnState != toPtr->pdmnState) {
     345        2043 :             CDeterministicMNStateDiff stateDiff(*fromPtr->pdmnState, *toPtr->pdmnState);
     346        2043 :             if (stateDiff.fields) {
     347        2043 :                 diffRet.updatedMNs.emplace(toPtr->GetInternalId(), std::move(stateDiff));
     348        2043 :             }
     349        2043 :         }
     350        3554 :     }
     351        9102 :     if (mnMap.size() + diffRet.addedMNs.size() != to.mnMap.size()) {
     352           7 :         for (auto& fromPtr : mnMap) {
     353           7 :             const auto toPtr = to.GetMN(fromPtr.second->proTxHash);
     354           7 :             if (toPtr == nullptr) {
     355           7 :                 diffRet.removedMns.emplace(fromPtr.second->GetInternalId());
     356           7 :                 if (mnMap.size() + diffRet.addedMNs.size() - diffRet.removedMns.size() == to.mnMap.size()) break;
     357           0 :             }
     358           7 :         };
     359           7 :     }
     360             : 
     361             :     // added MNs need to be sorted by internalId so that these are added in correct order when the diff is applied later
     362             :     // otherwise internalIds will not match with the original list
     363        9118 :     std::sort(diffRet.addedMNs.begin(), diffRet.addedMNs.end(), [](const CDeterministicMNCPtr& a, const CDeterministicMNCPtr& b) {
     364          16 :         return a->GetInternalId() < b->GetInternalId();
     365             :     });
     366             : 
     367        9102 :     return diffRet;
     368        9102 : }
     369             : 
     370         266 : void CDeterministicMNList::ApplyDiff(gsl::not_null<const CBlockIndex*> pindex, const CDeterministicMNListDiff& diff)
     371             : {
     372         266 :     blockHash = pindex->GetBlockHash();
     373         266 :     nHeight = pindex->nHeight;
     374             : 
     375         271 :     for (const auto& id : diff.removedMns) {
     376           5 :         auto dmn = GetMNByInternalId(id);
     377           5 :         if (!dmn) {
     378           0 :             throw std::runtime_error(strprintf("%s: can't find a removed masternode, id=%d", __func__, id));
     379             :         }
     380           5 :         RemoveMN(dmn->proTxHash);
     381           5 :     }
     382         283 :     for (const auto& dmn : diff.addedMNs) {
     383          17 :         AddMN(dmn);
     384             :     }
     385         270 :     for (const auto& p : diff.updatedMNs) {
     386           4 :         auto dmn = GetMNByInternalId(p.first);
     387           4 :         if (!dmn) {
     388           0 :             throw std::runtime_error(strprintf("%s: can't find an updated masternode, id=%d", __func__, p.first));
     389             :         }
     390           4 :         UpdateMN(*dmn, p.second);
     391           4 :     }
     392         266 : }
     393             : 
     394        1940 : void CDeterministicMNList::AddMN(const CDeterministicMNCPtr& dmn, bool fBumpTotalCount)
     395             : {
     396        1940 :     assert(dmn != nullptr);
     397             : 
     398        1940 :     if (mnMap.find(dmn->proTxHash)) {
     399        1344 :         throw(std::runtime_error(strprintf("%s: Can't add a masternode with a duplicate proTxHash=%s", __func__, dmn->proTxHash.ToString())));
     400             :     }
     401         596 :     if (mnInternalIdMap.find(dmn->GetInternalId())) {
     402           0 :         throw(std::runtime_error(strprintf("%s: Can't add a masternode with a duplicate internalId=%d", __func__, dmn->GetInternalId())));
     403             :     }
     404             : 
     405             :     // All mnUniquePropertyMap's updates must be atomic.
     406             :     // Using this temporary map as a checkpoint to roll back to in case of any issues.
     407         596 :     decltype(mnUniquePropertyMap) mnUniquePropertyMapSaved = mnUniquePropertyMap;
     408             : 
     409         596 :     if (!AddUniqueProperty(*dmn, dmn->collateralOutpoint)) {
     410           0 :         mnUniquePropertyMap = mnUniquePropertyMapSaved;
     411           0 :         throw(std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate collateralOutpoint=%s", __func__,
     412           0 :                 dmn->proTxHash.ToString(), dmn->collateralOutpoint.ToStringShort())));
     413             :     }
     414        1192 :     for (const auto& entry : dmn->pdmnState->netInfo->GetEntries()) {
     415        1192 :         if (const auto service_opt{entry.GetAddrPort()}) {
     416         596 :             if (!AddUniqueProperty(*dmn, *service_opt)) {
     417           0 :                 mnUniquePropertyMap = mnUniquePropertyMapSaved;
     418           0 :                 throw std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate address=%s", __func__,
     419           0 :                                                    dmn->proTxHash.ToString(), service_opt->ToStringAddrPort()));
     420             :             }
     421         596 :         } else if (const auto domain_opt{entry.GetDomainPort()}) {
     422           0 :             if (!AddUniqueProperty(*dmn, *domain_opt)) {
     423           0 :                 mnUniquePropertyMap = mnUniquePropertyMapSaved;
     424           0 :                 throw std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate address=%s",
     425           0 :                                                    __func__, dmn->proTxHash.ToString(), domain_opt->ToStringAddrPort()));
     426             :             }
     427           0 :         } else {
     428           0 :             mnUniquePropertyMap = mnUniquePropertyMapSaved;
     429           0 :             throw std::runtime_error(
     430           0 :                 strprintf("%s: Can't add a masternode %s with invalid address", __func__, dmn->proTxHash.ToString()));
     431             :         }
     432             :     }
     433         596 :     if (!AddUniqueProperty(*dmn, dmn->pdmnState->keyIDOwner)) {
     434           0 :         mnUniquePropertyMap = mnUniquePropertyMapSaved;
     435           0 :         throw(std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate keyIDOwner=%s", __func__,
     436           0 :                 dmn->proTxHash.ToString(), EncodeDestination(PKHash(dmn->pdmnState->keyIDOwner)))));
     437             :     }
     438         596 :     if (dmn->pdmnState->pubKeyOperator != CBLSLazyPublicKey() && !AddUniqueProperty(*dmn, dmn->pdmnState->pubKeyOperator)) {
     439           0 :         mnUniquePropertyMap = mnUniquePropertyMapSaved;
     440           0 :         throw(std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate pubKeyOperator=%s", __func__,
     441           0 :                 dmn->proTxHash.ToString(), dmn->pdmnState->pubKeyOperator.ToString())));
     442             :     }
     443             : 
     444         596 :     if (dmn->nType == MnType::Evo) {
     445           0 :         if (dmn->pdmnState->platformNodeID != uint160() && !AddUniqueProperty(*dmn, dmn->pdmnState->platformNodeID)) {
     446           0 :             mnUniquePropertyMap = mnUniquePropertyMapSaved;
     447           0 :             throw(std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate platformNodeID=%s", __func__,
     448           0 :                                                dmn->proTxHash.ToString(), dmn->pdmnState->platformNodeID.ToString())));
     449             :         }
     450           0 :     }
     451             : 
     452         596 :     mnMap = mnMap.set(dmn->proTxHash, dmn);
     453         596 :     mnInternalIdMap = mnInternalIdMap.set(dmn->GetInternalId(), dmn->proTxHash);
     454         596 :     InvalidateSMLCache();
     455         596 :     if (fBumpTotalCount) {
     456             :         // nTotalRegisteredCount acts more like a checkpoint, not as a limit,
     457         596 :         nTotalRegisteredCount = std::max(dmn->GetInternalId() + 1, (uint64_t)nTotalRegisteredCount);
     458         596 :     }
     459        1940 : }
     460             : 
     461        8333 : void CDeterministicMNList::UpdateMN(const CDeterministicMN& oldDmn, const std::shared_ptr<const CDeterministicMNState>& pdmnState)
     462             : {
     463        8333 :     auto dmn = std::make_shared<CDeterministicMN>(oldDmn);
     464        8333 :     auto oldState = dmn->pdmnState;
     465             : 
     466             :     // All mnUniquePropertyMap's updates must be atomic.
     467             :     // Using this temporary map as a checkpoint to roll back to in case of any issues.
     468        8333 :     decltype(mnUniquePropertyMap) mnUniquePropertyMapSaved = mnUniquePropertyMap;
     469             : 
     470       16666 :     auto updateNetInfo = [this](const CDeterministicMN& dmn, const std::shared_ptr<NetInfoInterface>& oldInfo,
     471             :                                 const std::shared_ptr<NetInfoInterface>& newInfo) -> std::string {
     472        8333 :         if (util::shared_ptr_not_equal(oldInfo, newInfo)) {
     473             :             // We track each individual entry in netInfo as opposed to netInfo itself (preventing us from
     474             :             // using UpdateUniqueProperty()), so we need to successfully purge all old entries and insert
     475             :             // new entries to successfully update.
     476          38 :             for (const auto& old_entry : oldInfo->GetEntries()) {
     477          34 :                 if (const auto service_opt{old_entry.GetAddrPort()}) {
     478          17 :                     if (!DeleteUniqueProperty(dmn, *service_opt)) {
     479           0 :                         return "internal error"; // This shouldn't be possible
     480             :                     }
     481          17 :                 } else if (const auto domain_opt{old_entry.GetDomainPort()}) {
     482           0 :                     if (!DeleteUniqueProperty(dmn, *domain_opt)) {
     483           0 :                         return "internal error"; // This shouldn't be possible
     484             :                     }
     485           0 :                 } else {
     486           0 :                     return "invalid address";
     487             :                 }
     488             :             }
     489          29 :             for (const auto& new_entry : newInfo->GetEntries()) {
     490          16 :                 if (const auto service_opt{new_entry.GetAddrPort()}) {
     491           8 :                     if (!AddUniqueProperty(dmn, *service_opt)) {
     492           0 :                         return strprintf("duplicate (%s)", service_opt->ToStringAddrPort());
     493             :                     }
     494           8 :                 } else if (const auto domain_opt{new_entry.GetDomainPort()}) {
     495           0 :                     if (!AddUniqueProperty(dmn, *domain_opt)) {
     496           0 :                         return strprintf("duplicate (%s)", domain_opt->ToStringAddrPort());
     497             :                     }
     498           0 :                 } else {
     499           0 :                     return "invalid address";
     500             :                 }
     501             :             }
     502          21 :         }
     503        8333 :         return "";
     504        8333 :     };
     505             : 
     506        8333 :     assert(oldState->netInfo && pdmnState->netInfo);
     507        8333 :     if (auto err = updateNetInfo(*dmn, oldState->netInfo, pdmnState->netInfo); !err.empty()) {
     508           0 :         mnUniquePropertyMap = mnUniquePropertyMapSaved;
     509           0 :         throw(std::runtime_error(strprintf("%s: Can't update masternode %s with addresses, reason=%s", __func__,
     510           0 :                                            oldDmn.proTxHash.ToString(), err)));
     511             :     }
     512        8333 :     if (!UpdateUniqueProperty(*dmn, oldState->keyIDOwner, pdmnState->keyIDOwner)) {
     513           0 :         mnUniquePropertyMap = mnUniquePropertyMapSaved;
     514           0 :         throw(std::runtime_error(strprintf("%s: Can't update a masternode %s with a duplicate keyIDOwner=%s", __func__,
     515           0 :                 oldDmn.proTxHash.ToString(), EncodeDestination(PKHash(pdmnState->keyIDOwner)))));
     516             :     }
     517        8333 :     if (!UpdateUniqueProperty(*dmn, oldState->pubKeyOperator, pdmnState->pubKeyOperator)) {
     518           0 :         mnUniquePropertyMap = mnUniquePropertyMapSaved;
     519           0 :         throw(std::runtime_error(strprintf("%s: Can't update a masternode %s with a duplicate pubKeyOperator=%s", __func__,
     520           0 :                 oldDmn.proTxHash.ToString(), pdmnState->pubKeyOperator.ToString())));
     521             :     }
     522        8333 :     if (dmn->nType == MnType::Evo) {
     523           0 :         if (!UpdateUniqueProperty(*dmn, oldState->platformNodeID, pdmnState->platformNodeID)) {
     524           0 :             mnUniquePropertyMap = mnUniquePropertyMapSaved;
     525           0 :             throw(std::runtime_error(strprintf("%s: Can't update a masternode %s with a duplicate platformNodeID=%s", __func__,
     526           0 :                                                oldDmn.proTxHash.ToString(), pdmnState->platformNodeID.ToString())));
     527             :         }
     528           0 :     }
     529             : 
     530        8333 :     dmn->pdmnState = pdmnState;
     531        8333 :     mnMap = mnMap.set(oldDmn.proTxHash, dmn);
     532        8333 :     LOCK(m_cached_sml_mutex);
     533       16507 :     if (m_cached_sml && oldDmn.to_sml_entry() != dmn->to_sml_entry()) {
     534          98 :         m_cached_sml = nullptr;
     535          98 :     }
     536        8333 : }
     537             : 
     538        8325 : void CDeterministicMNList::UpdateMN(const uint256& proTxHash, const std::shared_ptr<const CDeterministicMNState>& pdmnState)
     539             : {
     540        8325 :     auto oldDmn = mnMap.find(proTxHash);
     541        8325 :     if (!oldDmn) {
     542           0 :         throw(std::runtime_error(strprintf("%s: Can't find a masternode with proTxHash=%s", __func__, proTxHash.ToString())));
     543             :     }
     544        8325 :     UpdateMN(**oldDmn, pdmnState);
     545        8325 : }
     546             : 
     547           4 : void CDeterministicMNList::UpdateMN(const CDeterministicMN& oldDmn, const CDeterministicMNStateDiff& stateDiff)
     548             : {
     549           4 :     auto oldState = oldDmn.pdmnState;
     550           4 :     auto newState = std::make_shared<CDeterministicMNState>(*oldState);
     551           4 :     stateDiff.ApplyToState(*newState);
     552           4 :     UpdateMN(oldDmn, newState);
     553           4 : }
     554             : 
     555          12 : void CDeterministicMNList::RemoveMN(const uint256& proTxHash)
     556             : {
     557          12 :     auto dmn = GetMN(proTxHash);
     558          12 :     if (!dmn) {
     559           0 :         throw(std::runtime_error(strprintf("%s: Can't find a masternode with proTxHash=%s", __func__, proTxHash.ToString())));
     560             :     }
     561             : 
     562             :     // All mnUniquePropertyMap's updates must be atomic.
     563             :     // Using this temporary map as a checkpoint to roll back to in case of any issues.
     564          12 :     decltype(mnUniquePropertyMap) mnUniquePropertyMapSaved = mnUniquePropertyMap;
     565             : 
     566          12 :     if (!DeleteUniqueProperty(*dmn, dmn->collateralOutpoint)) {
     567           0 :         mnUniquePropertyMap = mnUniquePropertyMapSaved;
     568           0 :         throw(std::runtime_error(strprintf("%s: Can't delete a masternode %s with a collateralOutpoint=%s", __func__,
     569           0 :                 proTxHash.ToString(), dmn->collateralOutpoint.ToStringShort())));
     570             :     }
     571          17 :     for (const auto& entry : dmn->pdmnState->netInfo->GetEntries()) {
     572          10 :         if (const auto service_opt{entry.GetAddrPort()}) {
     573           5 :             if (!DeleteUniqueProperty(*dmn, *service_opt)) {
     574           0 :                 mnUniquePropertyMap = mnUniquePropertyMapSaved;
     575           0 :                 throw std::runtime_error(strprintf("%s: Can't delete a masternode %s with an address=%s", __func__,
     576           0 :                                                    proTxHash.ToString(), service_opt->ToStringAddrPort()));
     577             :             }
     578           5 :         } else if (const auto domain_opt{entry.GetDomainPort()}) {
     579           0 :             if (!DeleteUniqueProperty(*dmn, *domain_opt)) {
     580           0 :                 mnUniquePropertyMap = mnUniquePropertyMapSaved;
     581           0 :                 throw std::runtime_error(strprintf("%s: Can't delete a masternode %s with an address=%s", __func__,
     582           0 :                                                    proTxHash.ToString(), domain_opt->ToStringAddrPort()));
     583             :             }
     584           0 :         } else {
     585           0 :             mnUniquePropertyMap = mnUniquePropertyMapSaved;
     586           0 :             throw std::runtime_error(strprintf("%s: Can't delete a masternode %s with invalid address", __func__,
     587           0 :                                                dmn->proTxHash.ToString()));
     588             :         }
     589             :     }
     590          12 :     if (!DeleteUniqueProperty(*dmn, dmn->pdmnState->keyIDOwner)) {
     591           0 :         mnUniquePropertyMap = mnUniquePropertyMapSaved;
     592           0 :         throw(std::runtime_error(strprintf("%s: Can't delete a masternode %s with a keyIDOwner=%s", __func__,
     593           0 :                 proTxHash.ToString(), EncodeDestination(PKHash(dmn->pdmnState->keyIDOwner)))));
     594             :     }
     595          21 :     if (dmn->pdmnState->pubKeyOperator != CBLSLazyPublicKey() &&
     596           9 :         !DeleteUniqueProperty(*dmn, dmn->pdmnState->pubKeyOperator)) {
     597           0 :         mnUniquePropertyMap = mnUniquePropertyMapSaved;
     598           0 :         throw(std::runtime_error(strprintf("%s: Can't delete a masternode %s with a pubKeyOperator=%s", __func__,
     599           0 :                 proTxHash.ToString(), dmn->pdmnState->pubKeyOperator.ToString())));
     600             :     }
     601             : 
     602          12 :     if (dmn->nType == MnType::Evo) {
     603           0 :         if (dmn->pdmnState->platformNodeID != uint160() && !DeleteUniqueProperty(*dmn, dmn->pdmnState->platformNodeID)) {
     604           0 :             mnUniquePropertyMap = mnUniquePropertyMapSaved;
     605           0 :             throw(std::runtime_error(strprintf("%s: Can't delete a masternode %s with a platformNodeID=%s", __func__,
     606           0 :                                                dmn->proTxHash.ToString(), dmn->pdmnState->platformNodeID.ToString())));
     607             :         }
     608           0 :     }
     609             : 
     610          12 :     mnMap = mnMap.erase(proTxHash);
     611          12 :     mnInternalIdMap = mnInternalIdMap.erase(dmn->GetInternalId());
     612          12 :     InvalidateSMLCache();
     613          12 : }
     614             : 
     615         360 : CDeterministicMNManager::CDeterministicMNManager(CEvoDB& evoDb, CMasternodeMetaMan& mn_metaman) :
     616             :     m_evoDb{evoDb},
     617             :     m_mn_metaman{mn_metaman}
     618         180 : {
     619         180 : }
     620             : 
     621         360 : CDeterministicMNManager::~CDeterministicMNManager() = default;
     622             : 
     623        9083 : bool CDeterministicMNManager::ProcessBlock(const CBlock& block, gsl::not_null<const CBlockIndex*> pindex,
     624             :                                            BlockValidationState& state, const CDeterministicMNList& newList,
     625             :                                            std::optional<MNListUpdates>& updatesRet)
     626             : {
     627        9083 :     AssertLockHeld(::cs_main);
     628             : 
     629        9083 :     const auto& consensusParams = Params().GetConsensus();
     630        9083 :     if (!DeploymentActiveAt(*pindex, consensusParams, Consensus::DEPLOYMENT_DIP0003)) {
     631           0 :         return true;
     632             :     }
     633             : 
     634        9083 :     CDeterministicMNList oldList;
     635        9083 :     CDeterministicMNListDiff diff;
     636             : 
     637        9083 :     int nHeight = pindex->nHeight;
     638             : 
     639             :     try {
     640        9083 :         newList.to_sml(); // to populate the SML cache
     641             : 
     642        9083 :         LOCK(cs);
     643             : 
     644        9083 :         oldList = GetListForBlockInternal(pindex->pprev);
     645        9083 :         diff = oldList.BuildDiff(newList);
     646             : 
     647             :         // apply platform unban for platform revive too
     648       23598 :         for (int i = 1; i < (int)block.vtx.size(); i++) {
     649       14515 :             const CTransaction& tx = *block.vtx[i];
     650       14515 :             if (!tx.IsSpecialTxVersion() || tx.nType != TRANSACTION_PROVIDER_UPDATE_SERVICE) {
     651             :                 // only interested in revive transactions
     652       14511 :                 continue;
     653             :             }
     654           4 :             const auto opt_proTx = GetTxPayload<CProUpServTx>(tx);
     655           4 :             if (!opt_proTx) continue; // should not happen but does not matter
     656             : 
     657           4 :             if (!m_mn_metaman.ResetPlatformBan(opt_proTx->proTxHash, nHeight)) {
     658           4 :                 LogPrint(BCLog::LLMQ, "%s -- MN %s is failed to Platform revived at height %d\n", __func__,
     659             :                          opt_proTx->proTxHash.ToString(), nHeight);
     660           4 :             }
     661           4 :         }
     662             : 
     663        9083 :         m_evoDb.Write(std::make_pair(DB_LIST_DIFF, newList.GetBlockHash()), diff);
     664        9083 :         if ((nHeight % DISK_SNAPSHOT_PERIOD) == 0 || pindex->pprev == m_initial_snapshot_index) {
     665          18 :             m_evoDb.Write(std::make_pair(DB_LIST_SNAPSHOT, newList.GetBlockHash()), newList);
     666          18 :             mnListsCache.emplace(newList.GetBlockHash(), newList);
     667          18 :             LogPrintf("CDeterministicMNManager::%s -- Wrote snapshot. nHeight=%d, mapCurMNs.allMNsCount=%d\n",
     668             :                 __func__, nHeight, newList.GetCounts().total());
     669          18 :         }
     670             : 
     671        9083 :         diff.nHeight = pindex->nHeight;
     672        9083 :         mnListDiffsCache.emplace(pindex->GetBlockHash(), diff);
     673        9083 :         mnListsCache.emplace(newList.GetBlockHash(), newList);
     674        9083 :     } catch (const std::exception& e) {
     675           0 :         LogPrintf("CDeterministicMNManager::%s -- internal error: %s\n", __func__, e.what());
     676           0 :         return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-dmn-block");
     677           0 :     }
     678             : 
     679        9083 :     if (diff.HasChanges()) {
     680        2017 :         updatesRet = {.old_list = oldList, .new_list = newList, .diff = diff};
     681        2017 :     }
     682             : 
     683        9083 :     if (::g_stats_client->active()) {
     684           0 :         const auto counts{newList.GetCounts()};
     685           0 :         ::g_stats_client->gauge("masternodes.count", counts.total());
     686           0 :         ::g_stats_client->gauge("masternodes.weighted_count", counts.m_total_weighted);
     687           0 :         ::g_stats_client->gauge("masternodes.enabled", counts.enabled());
     688           0 :         ::g_stats_client->gauge("masternodes.weighted_enabled", counts.m_valid_weighted);
     689           0 :         ::g_stats_client->gauge("masternodes.evo.count", counts.m_total_evo);
     690           0 :         ::g_stats_client->gauge("masternodes.evo.enabled", counts.m_valid_evo);
     691           0 :         ::g_stats_client->gauge("masternodes.mn.count", counts.m_total_mn);
     692           0 :         ::g_stats_client->gauge("masternodes.mn.enabled", counts.m_valid_mn);
     693           0 :     }
     694             : 
     695        9083 :     if (nHeight == consensusParams.DIP0003EnforcementHeight) {
     696          16 :         if (!consensusParams.DIP0003EnforcementHash.IsNull() && consensusParams.DIP0003EnforcementHash != pindex->GetBlockHash()) {
     697           0 :             LogPrintf("CDeterministicMNManager::%s -- DIP3 enforcement block has wrong hash: hash=%s, expected=%s, nHeight=%d\n", __func__,
     698             :                     pindex->GetBlockHash().ToString(), consensusParams.DIP0003EnforcementHash.ToString(), nHeight);
     699           0 :             return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-dip3-enf-block");
     700             :         }
     701          16 :         LogPrintf("CDeterministicMNManager::%s -- DIP3 is enforced now. nHeight=%d\n", __func__, nHeight);
     702          16 :     }
     703        9083 :     int current = to_cleanup.load();
     704        9083 :     while (nHeight > current && !to_cleanup.compare_exchange_weak(current, nHeight)) {
     705             :         // Loop continues if compare_exchange_weak failed (another thread changed it) (current is updated to the new value in to_cleanup)
     706             :     }
     707        9083 :     return true;
     708        9083 : }
     709             : 
     710         389 : bool CDeterministicMNManager::UndoBlock(gsl::not_null<const CBlockIndex*> pindex, std::optional<MNListUpdates>& updatesRet)
     711             : {
     712         389 :     int nHeight = pindex->nHeight;
     713         389 :     uint256 blockHash = pindex->GetBlockHash();
     714             : 
     715         389 :     CDeterministicMNList prevList;
     716         389 :     CDeterministicMNListDiff diff;
     717             :     {
     718         389 :         LOCK(cs);
     719         389 :         m_evoDb.Read(std::make_pair(DB_LIST_DIFF, blockHash), diff);
     720             : 
     721         389 :         if (diff.HasChanges()) {
     722             :             // need to call this before erasing
     723           2 :             prevList = GetListForBlockInternal(pindex->pprev);
     724           2 :         }
     725             : 
     726         389 :         mnListsCache.erase(blockHash);
     727         389 :         mnListDiffsCache.erase(blockHash);
     728         389 :     }
     729         389 :     if (diff.HasChanges()) {
     730           2 :         CDeterministicMNList curList{prevList};
     731           2 :         curList.ApplyDiff(pindex, diff);
     732             : 
     733           2 :         auto inversedDiff{curList.BuildDiff(prevList)};
     734           2 :         updatesRet = {.old_list = curList, .new_list = prevList, .diff = inversedDiff};
     735           2 :     }
     736             : 
     737         389 :     const auto& consensusParams = Params().GetConsensus();
     738         389 :     if (nHeight == consensusParams.DIP0003EnforcementHeight) {
     739           0 :         LogPrintf("CDeterministicMNManager::%s -- DIP3 is not enforced anymore. nHeight=%d\n", __func__, nHeight);
     740           0 :     }
     741             : 
     742             :     return true;
     743         389 : }
     744             : 
     745         672 : void CDeterministicMNManager::UpdatedBlockTip(gsl::not_null<const CBlockIndex*> pindex)
     746             : {
     747         672 :     LOCK(cs);
     748             : 
     749         672 :     tipIndex = pindex;
     750         672 : }
     751             : 
     752       96385 : CDeterministicMNList CDeterministicMNManager::GetListForBlockInternal(gsl::not_null<const CBlockIndex*> pindex)
     753             : {
     754       96385 :     CDeterministicMNList snapshot;
     755             : 
     756       96385 :     if (!DeploymentActiveAt(*pindex, Params().GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) {
     757       16305 :         return snapshot;
     758             :     }
     759             : 
     760       80080 :     AssertLockHeld(cs);
     761             : 
     762       80080 :     std::list<const CBlockIndex*> listDiffIndexes;
     763             : 
     764       80327 :     while (true) {
     765             :         // try using cache before reading from disk
     766       80327 :         auto itLists = mnListsCache.find(pindex->GetBlockHash());
     767       80327 :         if (itLists != mnListsCache.end()) {
     768       80080 :             snapshot = itLists->second;
     769       80080 :             break;
     770             :         }
     771             : 
     772         247 :         if (m_evoDb.Read(std::make_pair(DB_LIST_SNAPSHOT, pindex->GetBlockHash()), snapshot)) {
     773           0 :             mnListsCache.emplace(pindex->GetBlockHash(), snapshot);
     774           0 :             break;
     775             :         }
     776             : 
     777             :         // no snapshot found yet, check diffs
     778         247 :         auto itDiffs = mnListDiffsCache.find(pindex->GetBlockHash());
     779         247 :         if (itDiffs != mnListDiffsCache.end()) {
     780         247 :             listDiffIndexes.emplace_front(pindex);
     781         247 :             pindex = pindex->pprev;
     782         247 :             continue;
     783             :         }
     784             : 
     785           0 :         CDeterministicMNListDiff diff;
     786           0 :         if (!m_evoDb.Read(std::make_pair(DB_LIST_DIFF, pindex->GetBlockHash()), diff)) {
     787             :             // no snapshot and no diff on disk means that it's the initial snapshot
     788           0 :             m_initial_snapshot_index = pindex;
     789           0 :             snapshot = CDeterministicMNList(pindex->GetBlockHash(), pindex->nHeight, 0);
     790           0 :             mnListsCache.emplace(pindex->GetBlockHash(), snapshot);
     791           0 :             LogPrintf("CDeterministicMNManager::%s -- initial snapshot. blockHash=%s nHeight=%d\n",
     792             :                     __func__, snapshot.GetBlockHash().ToString(), snapshot.GetHeight());
     793           0 :             break;
     794             :         }
     795             : 
     796           0 :         diff.nHeight = pindex->nHeight;
     797           0 :         mnListDiffsCache.emplace(pindex->GetBlockHash(), std::move(diff));
     798           0 :         listDiffIndexes.emplace_front(pindex);
     799           0 :         pindex = pindex->pprev;
     800           0 :     }
     801             : 
     802       80327 :     for (const auto& diffIndex : listDiffIndexes) {
     803         247 :         const auto& diff = mnListDiffsCache.at(diffIndex->GetBlockHash());
     804         247 :         snapshot.ApplyDiff(diffIndex, diff);
     805             : 
     806             :         static constexpr int MINI_SNAPSHOT_INTERVAL = 32;
     807         247 :         if (!node::fReindex && snapshot.GetHeight() % MINI_SNAPSHOT_INTERVAL == 0) {
     808             :             // Add this temporary mini-snapshot to the cache.
     809             :             // Persistent masternode list snapshots are stored in evo-db every 576 blocks.
     810             :             // To answer GetListForBlock() between these snapshots, the node must rebuild
     811             :             // state by applying up to 575 diffs from the nearest persistent snapshot.
     812             :             // If GetListForBlock() is called repeatedly in that range, the work multiplies
     813             :             // (up to 575 diffs * number of calls).
     814             :             // Mini-snapshots reduce this overhead by caching intermediate states
     815             :             // every MINI_SNAPSHOT_INTERVAL blocks. Unlike persistent snapshots, these live
     816             :             // only in memory and are cleaned up after a short time by the scheduled cleanup().
     817             :             // There is also separate in-memory caching for the current tip and active quorums,
     818             :             // but this mini-snapshot cache specifically speeds up repeated requests
     819             :             // for nearby historical blocks.
     820           0 :             mnListsCache.emplace(snapshot.GetBlockHash(), snapshot);
     821           0 :         }
     822             :     }
     823             : 
     824       80080 :     if (tipIndex) {
     825             :         // always keep a snapshot for the tip
     826       25385 :         if (const auto snapshot_hash = snapshot.GetBlockHash(); snapshot_hash == tipIndex->GetBlockHash()) {
     827       13591 :             mnListsCache.emplace(snapshot_hash, snapshot);
     828       13591 :         } else {
     829             :             // keep snapshots for yet alive quorums
     830       68924 :             if (std::ranges::any_of(Params().GetConsensus().llmqs, [&snapshot, this](
     831             :                                                                        const auto& params) EXCLUSIVE_LOCKS_REQUIRED(cs) {
     832       57130 :                     AssertLockHeld(cs);
     833       57590 :                     return (snapshot.GetHeight() % params.dkgInterval == 0) &&
     834         920 :                            (snapshot.GetHeight() + params.dkgInterval * (params.keepOldConnections + 1) >=
     835         460 :                             tipIndex->nHeight);
     836             :                 })) {
     837         460 :                 mnListsCache.emplace(snapshot_hash, snapshot);
     838         460 :             }
     839             :         }
     840       25385 :     }
     841             : 
     842       80080 :     assert(snapshot.GetHeight() != -1);
     843       80080 :     return snapshot;
     844       96385 : }
     845             : 
     846       63943 : CDeterministicMNList CDeterministicMNManager::GetListAtChainTip()
     847             : {
     848       63943 :     LOCK(cs);
     849       63943 :     if (!tipIndex) {
     850       55714 :         return {};
     851             :     }
     852        8229 :     return GetListForBlockInternal(tipIndex);
     853       63943 : }
     854             : 
     855         882 : bool CDeterministicMNManager::IsProTxWithCollateral(const CTransactionRef& tx, uint32_t n)
     856             : {
     857         882 :     if (!tx->IsSpecialTxVersion() || tx->nType != TRANSACTION_PROVIDER_REGISTER) {
     858         882 :         return false;
     859             :     }
     860           0 :     const auto opt_proTx = GetTxPayload<CProRegTx>(*tx);
     861           0 :     if (!opt_proTx) {
     862           0 :         return false;
     863             :     }
     864           0 :     auto& proTx = *opt_proTx;
     865             : 
     866           0 :     if (!proTx.collateralOutpoint.hash.IsNull()) {
     867           0 :         return false;
     868             :     }
     869           0 :     if (proTx.collateralOutpoint.n >= tx->vout.size() || proTx.collateralOutpoint.n != n) {
     870           0 :         return false;
     871             :     }
     872             : 
     873             : 
     874           0 :     if (const CAmount expectedCollateral = GetMnType(proTx.nType).collat_amount; tx->vout[n].nValue != expectedCollateral) {
     875           0 :         return false;
     876             :     }
     877           0 :     return true;
     878         882 : }
     879             : 
     880          15 : void CDeterministicMNManager::CleanupCache(int nHeight)
     881             : {
     882          15 :     AssertLockHeld(cs);
     883             : 
     884          15 :     std::vector<uint256> toDeleteLists;
     885          15 :     std::vector<uint256> toDeleteDiffs;
     886         153 :     for (const auto& p : mnListsCache) {
     887         138 :         if (p.second.GetHeight() + LIST_DIFFS_CACHE_SIZE < nHeight) {
     888             :             // too old, drop it
     889           0 :             toDeleteLists.emplace_back(p.first);
     890           0 :             continue;
     891             :         }
     892         276 :         if (tipIndex != nullptr && p.first == tipIndex->GetBlockHash()) {
     893             :             // it's a snapshot for the tip, keep it
     894          15 :             continue;
     895             :         }
     896         538 :         bool fQuorumCache = std::ranges::any_of(Params().GetConsensus().llmqs, [&nHeight, &p](const auto& params) {
     897         465 :             return (p.second.GetHeight() % params.dkgInterval == 0) &&
     898          50 :                    (p.second.GetHeight() + params.dkgInterval * (params.keepOldConnections + 1) >= nHeight);
     899             :         });
     900         123 :         if (fQuorumCache) {
     901             :             // at least one quorum could be using it, keep it
     902          50 :             continue;
     903             :         }
     904             :         // none of the above, drop it
     905          73 :         toDeleteLists.emplace_back(p.first);
     906             :     }
     907          88 :     for (const auto& h : toDeleteLists) {
     908          73 :         mnListsCache.erase(h);
     909             :     }
     910        1080 :     for (const auto& p : mnListDiffsCache) {
     911        1065 :         if (p.second.nHeight + LIST_DIFFS_CACHE_SIZE < nHeight) {
     912           0 :             toDeleteDiffs.emplace_back(p.first);
     913           0 :         }
     914             :     }
     915          15 :     for (const auto& h : toDeleteDiffs) {
     916           0 :         mnListDiffsCache.erase(h);
     917             :     }
     918             : 
     919          15 : }
     920             : 
     921             : //end
     922             : 
     923          15 : void CDeterministicMNManager::DoMaintenance() {
     924          15 :     LOCK(cs_cleanup);
     925          15 :     int loc_to_cleanup = to_cleanup.load();
     926          15 :     if (loc_to_cleanup <= did_cleanup) return;
     927          15 :     LOCK(cs);
     928          15 :     CleanupCache(loc_to_cleanup);
     929          15 :     did_cleanup = loc_to_cleanup;
     930          15 : }
     931             : 
     932         178 : bool CDeterministicMNManager::IsMigrationRequired() const
     933             : {
     934             :     // Check if there are any legacy format diffs in the database
     935             :     // by looking for DB_LIST_DIFF_LEGACY entries
     936             : 
     937         178 :     AssertLockHeld(::cs_main);
     938             : 
     939         178 :     std::unique_ptr<CDBIterator> pcursor{m_evoDb.GetRawDB().NewIterator()};
     940         178 :     auto start{std::make_tuple(DB_LIST_DIFF_LEGACY, uint256{})};
     941         178 :     pcursor->Seek(start);
     942             : 
     943             :     // If we find any entries with the legacy key, migration is needed
     944         178 :     if (pcursor->Valid()) {
     945           0 :         decltype(start) k;
     946           0 :         if (pcursor->GetKey(k) && std::get<0>(k) == DB_LIST_DIFF_LEGACY) {
     947           0 :             LogPrintf("CDeterministicMNManager::%s -- Migration to nVersion-first format is needed\n", __func__);
     948           0 :             pcursor.reset();
     949           0 :             return true;
     950             :         }
     951           0 :     }
     952             : 
     953         178 :     LogPrintf("CDeterministicMNManager::%s -- Migration to nVersion-first format is not needed\n", __func__);
     954         178 :     pcursor.reset();
     955         178 :     return false; // No legacy format found
     956         178 : }
     957             : 
     958           0 : bool CDeterministicMNManager::MigrateLegacyDiffs(const CBlockIndex* const tip_index)
     959             : {
     960             :     // CRITICAL: This migration converts ALL stored CDeterministicMNListDiff entries
     961             :     // from legacy database key (DB_LIST_DIFF_LEGACY) to new key (DB_LIST_DIFF) format
     962             : 
     963           0 :     AssertLockHeld(::cs_main);
     964             : 
     965           0 :     LogPrintf("CDeterministicMNManager::%s -- Starting migration to nVersion-first format\n", __func__);
     966             : 
     967           0 :     std::vector<const CBlockIndex*> keys_to_erase;
     968             : 
     969           0 :     CDBBatch batch(m_evoDb.GetRawDB());
     970           0 :     std::unique_ptr<CDBIterator> pcursor{m_evoDb.GetRawDB().NewIterator()};
     971             : 
     972             :     // Keep track of the list to get correct nVersion at the current height
     973           0 :     CDeterministicMNList snapshot;
     974             : 
     975           0 :     const auto start_height{Params().GetConsensus().DeploymentHeight(Consensus::DEPLOYMENT_DIP0003)};
     976           0 :     for (auto current_height : std::views::iota(start_height, tip_index->nHeight + 1)) {
     977           0 :         auto current_index = tip_index->GetAncestor(current_height);
     978           0 :         auto target_key{std::make_tuple(DB_LIST_DIFF_LEGACY, current_index->GetBlockHash())};
     979           0 :         pcursor->Seek(target_key);
     980             : 
     981           0 :         decltype(target_key) key;
     982           0 :         if (!pcursor->Valid() || !pcursor->GetKey(key) || std::get<0>(key) != DB_LIST_DIFF_LEGACY) {
     983           0 :             break;
     984             :         }
     985             : 
     986           0 :         if (std::get<1>(key) != current_index->GetBlockHash()) {
     987           0 :             throw std::ios_base::failure("Invalid data, we must have legacy diffs for each height");
     988             :         }
     989             : 
     990             :         // Use legacy-aware deserialization for DB_LIST_DIFF_LEGACY entries
     991           0 :         CDataStream s(SER_DISK, CLIENT_VERSION);
     992           0 :         if (!m_evoDb.GetRawDB().ReadDataStream(key, s)) {
     993           0 :             break;
     994             :         }
     995             : 
     996           0 :         CDeterministicMNListDiff legacyDiff;
     997           0 :         legacyDiff.UnserializeLegacyFormat(s); // Use legacy format deserializer
     998           0 :         snapshot.ApplyDiff(current_index, legacyDiff);
     999             : 
    1000           0 :         CDeterministicMNListDiff convertedDiff;
    1001           0 :         convertedDiff.addedMNs = legacyDiff.addedMNs;
    1002           0 :         convertedDiff.removedMns = legacyDiff.removedMns;
    1003             : 
    1004             :         // The conversion is already done by UnserializeLegacyFormat()!
    1005             :         // CDeterministicMNStateDiffLegacy.ToNewFormat() was called during deserialization
    1006             :         // So legacyDiff.updatedMNs already contains properly converted CDeterministicMNStateDiff objects
    1007             : 
    1008             :         // Copy the already-converted state diffs but make sure pubKeyOperator, nVersion and fields are set properly
    1009           0 :         for (auto& [internalId, stateDiff] : legacyDiff.updatedMNs) {
    1010           0 :             auto dmn = snapshot.GetMNByInternalId(internalId);
    1011           0 :             if (!dmn) {
    1012             :                 // shouldn't happen
    1013           0 :                 throw std::runtime_error(strprintf("%s: can't find an updated masternode, id=%d", __func__, internalId));
    1014             :             }
    1015           0 :             if (!(stateDiff.fields & CDeterministicMNStateDiff::Field_nVersion)) {
    1016           0 :                 if ((stateDiff.fields & CDeterministicMNStateDiff::Field_pubKeyOperator) ||
    1017           0 :                     (stateDiff.fields & CDeterministicMNStateDiff::Field_netInfo)) {
    1018           0 :                     stateDiff.fields |= CDeterministicMNStateDiff::Field_nVersion;
    1019           0 :                     stateDiff.state.nVersion = dmn->pdmnState->nVersion;
    1020           0 :                 }
    1021           0 :             }
    1022           0 :             if (stateDiff.fields & CDeterministicMNStateDiff::Field_pubKeyOperator) {
    1023           0 :                 stateDiff.state.pubKeyOperator.SetLegacy(stateDiff.state.nVersion == ProTxVersion::LegacyBLS);
    1024           0 :             }
    1025           0 :             convertedDiff.updatedMNs.emplace(internalId, stateDiff);
    1026           0 :         }
    1027             : 
    1028             :         // Write the converted diff to new database key
    1029           0 :         batch.Write(std::make_pair(DB_LIST_DIFF, std::get<1>(key)), convertedDiff);
    1030           0 :         keys_to_erase.push_back(current_index);
    1031             : 
    1032           0 :         if (batch.SizeEstimate() >= (1 << 24)) {
    1033           0 :             LogPrintf("CDeterministicMNManager::%s -- Writing new diffs, height=%d...\n", __func__, current_height);
    1034           0 :             m_evoDb.GetRawDB().WriteBatch(batch);
    1035           0 :             batch.Clear();
    1036           0 :         }
    1037           0 :     }
    1038           0 :     pcursor.reset();
    1039             : 
    1040           0 :     LogPrintf("CDeterministicMNManager::%s -- Writing new diffs, height=%d...\n", __func__, tip_index->nHeight);
    1041           0 :     m_evoDb.GetRawDB().WriteBatch(batch);
    1042           0 :     batch.Clear();
    1043           0 :     LogPrintf("CDeterministicMNManager::%s -- Wrote %d new diffs\n", __func__, keys_to_erase.size());
    1044             : 
    1045             :     // Delete all found legacy format entries
    1046           0 :     for (const auto& index : keys_to_erase) {
    1047           0 :         batch.Erase(std::make_pair(DB_LIST_DIFF_LEGACY, index->GetBlockHash()));
    1048             : 
    1049           0 :         if (batch.SizeEstimate() >= (1 << 24)) {
    1050           0 :             LogPrintf("CDeterministicMNManager::%s -- Erasing found legacy diffs, height=%d...\n", __func__,
    1051             :                       index->nHeight);
    1052           0 :             m_evoDb.GetRawDB().WriteBatch(batch);
    1053           0 :             batch.Clear();
    1054           0 :         }
    1055             :     }
    1056             : 
    1057           0 :     LogPrintf("CDeterministicMNManager::%s -- Erasing found legacy diffs, height=%d...\n", __func__, tip_index->nHeight);
    1058           0 :     m_evoDb.GetRawDB().WriteBatch(batch);
    1059           0 :     batch.Clear();
    1060           0 :     LogPrintf("CDeterministicMNManager::%s -- Erased %d found legacy diffs\n", __func__, keys_to_erase.size());
    1061             : 
    1062             :     // Delete all dangling legacy format entries
    1063           0 :     std::unique_ptr<CDBIterator> pcursor_dangling{m_evoDb.GetRawDB().NewIterator()};
    1064           0 :     auto start{std::make_tuple(DB_LIST_DIFF_LEGACY, uint256{})};
    1065           0 :     pcursor_dangling->Seek(start);
    1066           0 :     int count{0};
    1067           0 :     while (pcursor_dangling->Valid()) {
    1068           0 :         decltype(start) key;
    1069           0 :         if (!pcursor_dangling->GetKey(key) || std::get<0>(key) != DB_LIST_DIFF_LEGACY) {
    1070           0 :             break;
    1071             :         }
    1072           0 :         LogPrintf("CDeterministicMNManager::%s -- Erasing dangling legacy diff, hash=%s\n", __func__,
    1073             :                   std::get<1>(key).ToString());
    1074           0 :         batch.Erase(std::make_pair(DB_LIST_DIFF_LEGACY, std::get<1>(key)));
    1075           0 :         pcursor_dangling->Next();
    1076           0 :         ++count;
    1077           0 :     }
    1078           0 :     pcursor_dangling.reset();
    1079             : 
    1080           0 :     m_evoDb.GetRawDB().WriteBatch(batch);
    1081           0 :     batch.Clear();
    1082           0 :     LogPrintf("CDeterministicMNManager::%s -- Erased %d dangling legacy diffs\n", __func__, count);
    1083             : 
    1084           0 :     LogPrintf("CDeterministicMNManager::%s -- Compacting database...\n", __func__);
    1085           0 :     m_evoDb.GetRawDB().CompactFull();
    1086             : 
    1087             :     // flush it to disk
    1088           0 :     if (!m_evoDb.CommitRootTransaction()) {
    1089           0 :         LogPrintf("CDeterministicMNManager::%s -- Failed to commit to evoDB\n", __func__);
    1090           0 :         return false;
    1091             :     }
    1092             : 
    1093             :     // Clear caches to force reload with new format
    1094           0 :     LOCK(cs);
    1095           0 :     mnListsCache.clear();
    1096           0 :     mnListDiffsCache.clear();
    1097             : 
    1098           0 :     LogPrintf("CDeterministicMNManager::%s -- Successfully migrated %d diffs to nVersion-first format\n", __func__,
    1099             :               keys_to_erase.size());
    1100             : 
    1101           0 :     return true;
    1102           0 : }
    1103             : 
    1104           0 : CDeterministicMNManager::RecalcDiffsResult CDeterministicMNManager::RecalculateAndRepairDiffs(
    1105             :     const CBlockIndex* start_index, const CBlockIndex* stop_index, ChainstateManager& chainman,
    1106             :     BuildListFromBlockFunc build_list_func, bool repair)
    1107             : {
    1108           0 :     RecalcDiffsResult result;
    1109           0 :     result.start_height = start_index->nHeight;
    1110           0 :     result.stop_height = stop_index->nHeight;
    1111             : 
    1112           0 :     const auto& consensus_params = Params().GetConsensus();
    1113             : 
    1114             :     // Clamp start height to DIP0003 activation (no snapshots/diffs exist before this)
    1115           0 :     if (start_index->nHeight < consensus_params.DIP0003Height) {
    1116           0 :         start_index = stop_index->GetAncestor(consensus_params.DIP0003Height);
    1117           0 :         if (!start_index) {
    1118           0 :             result.verification_errors.push_back(strprintf("Stop height %d is below DIP0003 activation height %d",
    1119           0 :                                                            stop_index->nHeight, consensus_params.DIP0003Height));
    1120           0 :             return result;
    1121             :         }
    1122           0 :         LogPrintf("CDeterministicMNManager::%s -- Clamped start height from %d to DIP0003 activation height %d\n",
    1123             :                   __func__, result.start_height, consensus_params.DIP0003Height);
    1124             :         // Update result to reflect the clamped start height
    1125           0 :         result.start_height = start_index->nHeight;
    1126           0 :     }
    1127             : 
    1128             :     // Collect all snapshot blocks in the range
    1129           0 :     std::vector<const CBlockIndex*> snapshot_blocks = CollectSnapshotBlocks(start_index, stop_index, consensus_params);
    1130             : 
    1131           0 :     if (snapshot_blocks.empty()) {
    1132           0 :         result.verification_errors.push_back("Could not find starting snapshot");
    1133           0 :         return result;
    1134             :     }
    1135             : 
    1136           0 :     if (snapshot_blocks.size() < 2) {
    1137           0 :         result.verification_errors.push_back(strprintf("Need at least 2 snapshots, found %d", snapshot_blocks.size()));
    1138           0 :         return result;
    1139             :     }
    1140             : 
    1141           0 :     LogPrintf("CDeterministicMNManager::%s -- Processing %d snapshot pairs between heights %d and %d\n", __func__,
    1142             :               snapshot_blocks.size() - 1, result.start_height, result.stop_height);
    1143             : 
    1144             :     // Storage for recalculated diffs if we plan to repair
    1145           0 :     std::vector<std::pair<uint256, CDeterministicMNListDiff>> recalculated_diffs;
    1146             : 
    1147             :     // Process each pair of consecutive snapshots
    1148           0 :     for (size_t i = 0; i < snapshot_blocks.size() - 1; ++i) {
    1149           0 :         const CBlockIndex* from_index = snapshot_blocks[i];
    1150           0 :         const CBlockIndex* to_index = snapshot_blocks[i + 1];
    1151             : 
    1152             :         // Load the snapshots from disk
    1153           0 :         CDeterministicMNList from_snapshot;
    1154           0 :         CDeterministicMNList to_snapshot;
    1155             : 
    1156           0 :         bool has_from_snapshot = m_evoDb.Read(std::make_pair(DB_LIST_SNAPSHOT, from_index->GetBlockHash()), from_snapshot);
    1157           0 :         bool has_to_snapshot = m_evoDb.Read(std::make_pair(DB_LIST_SNAPSHOT, to_index->GetBlockHash()), to_snapshot);
    1158             : 
    1159             :         // Handle missing snapshots
    1160           0 :         if (!has_from_snapshot) {
    1161             :             // The initial snapshot at DIP0003 activation might not exist in the database on nodes
    1162             :             // that synced before the fix to explicitly write it. This is the only acceptable case.
    1163           0 :             if (from_index->nHeight == consensus_params.DIP0003Height) {
    1164             :                 // Create an empty initial snapshot (matching what GetListForBlockInternal does)
    1165           0 :                 from_snapshot = CDeterministicMNList(from_index->GetBlockHash(), from_index->nHeight, 0);
    1166           0 :                 LogPrintf("CDeterministicMNManager::%s -- Using empty initial snapshot at DIP0003 height %d\n",
    1167             :                           __func__, from_index->nHeight);
    1168           0 :             } else {
    1169             :                 // Any other missing snapshot is critical corruption beyond our repair capability
    1170           0 :                 result.verification_errors.push_back(strprintf("CRITICAL: Snapshot missing at height %d. "
    1171           0 :                     "This cannot be repaired by this tool - full reindex required.", from_index->nHeight));
    1172           0 :                 return result;
    1173             :             }
    1174           0 :         }
    1175             : 
    1176           0 :         if (!has_to_snapshot) {
    1177             :             // Missing target snapshot is always critical - we cannot repair snapshots, only diffs
    1178           0 :             result.verification_errors.push_back(strprintf("CRITICAL: Snapshot missing at height %d. "
    1179           0 :                 "This cannot be repaired by this tool - full reindex required.", to_index->nHeight));
    1180           0 :             return result;
    1181             :         }
    1182             : 
    1183             :         // Log progress periodically (every 100 snapshot pairs) to avoid spam
    1184           0 :         if (i % 100 == 0) {
    1185           0 :             LogPrintf("CDeterministicMNManager::%s -- Progress: verifying snapshot pair %d/%d (heights %d-%d)\n",
    1186             :                       __func__, i + 1, snapshot_blocks.size() - 1, from_index->nHeight, to_index->nHeight);
    1187           0 :         }
    1188             : 
    1189             :         // Verify this snapshot pair
    1190           0 :         bool is_snapshot_pair_valid = VerifySnapshotPair(from_index, to_index, from_snapshot, to_snapshot, result);
    1191             : 
    1192             :         // If repair mode is enabled and verification failed, recalculate diffs from blockchain
    1193           0 :         if (repair && !is_snapshot_pair_valid) {
    1194           0 :             auto temp_diffs = RepairSnapshotPair(from_index, to_index, from_snapshot, to_snapshot, build_list_func, result);
    1195           0 :             if (temp_diffs.empty()) {
    1196             :                 // RepairSnapshotPair failed - this is a critical error, cannot continue
    1197           0 :                 return result;
    1198             :             }
    1199             :             // Only commit diffs if recalculation verification passed
    1200           0 :             recalculated_diffs.insert(recalculated_diffs.end(), temp_diffs.begin(), temp_diffs.end());
    1201           0 :             result.diffs_recalculated += temp_diffs.size();
    1202           0 :         }
    1203           0 :     }
    1204             : 
    1205             :     // Write repaired diffs to database
    1206           0 :     if (repair) {
    1207           0 :         WriteRepairedDiffs(recalculated_diffs, result);
    1208           0 :     }
    1209             : 
    1210           0 :     return result;
    1211           0 : }
    1212             : 
    1213           0 : bool CDeterministicMNManager::IsRepaired() const { return m_evoDb.Exists(DB_LIST_REPAIRED); }
    1214             : 
    1215           0 : void CDeterministicMNManager::CompleteRepair()
    1216             : {
    1217           0 :     auto dbTx = m_evoDb.BeginTransaction();
    1218           0 :     m_evoDb.Write(DB_LIST_REPAIRED, 1);
    1219           0 :     dbTx->Commit();
    1220             :     // flush it to disk
    1221           0 :     if (!m_evoDb.CommitRootTransaction()) {
    1222           0 :         LogPrintf("CDeterministicMNManager::%s -- Failed to commit to evoDB\n", __func__);
    1223           0 :         assert(false);
    1224             :     }
    1225           0 : }
    1226             : 
    1227           0 : std::vector<const CBlockIndex*> CDeterministicMNManager::CollectSnapshotBlocks(
    1228             :     const CBlockIndex* start_index, const CBlockIndex* stop_index, const Consensus::Params& consensus_params)
    1229             : {
    1230           0 :     std::vector<const CBlockIndex*> snapshot_blocks;
    1231             : 
    1232             :     // Add the starting snapshot (find the snapshot at or before start)
    1233             :     // Walk backwards to find a snapshot block (divisible by DISK_SNAPSHOT_PERIOD)
    1234             :     // or the initial snapshot at DIP0003 activation height
    1235           0 :     const CBlockIndex* snapshot_start_index = start_index;
    1236           0 :     while (snapshot_start_index && snapshot_start_index->nHeight > consensus_params.DIP0003Height &&
    1237           0 :            (snapshot_start_index->nHeight % DISK_SNAPSHOT_PERIOD) != 0) {
    1238           0 :         snapshot_start_index = snapshot_start_index->pprev;
    1239             :     }
    1240             : 
    1241           0 :     if (!snapshot_start_index) {
    1242           0 :         return snapshot_blocks; // Empty vector indicates error
    1243             :     }
    1244             : 
    1245             :     // Collect all snapshot blocks up to and including the stop block
    1246           0 :     snapshot_blocks.push_back(snapshot_start_index);
    1247             : 
    1248             :     // Find all subsequent snapshot heights
    1249           0 :     int current_snapshot_height = snapshot_start_index->nHeight;
    1250           0 :     while (true) {
    1251             :         // Calculate next snapshot height
    1252             :         int next_snapshot_height;
    1253           0 :         if (current_snapshot_height == consensus_params.DIP0003Height) {
    1254             :             // If we're at DIP0003 activation (initial snapshot), next is at first regular interval
    1255           0 :             next_snapshot_height = ((consensus_params.DIP0003Height / DISK_SNAPSHOT_PERIOD) + 1) * DISK_SNAPSHOT_PERIOD;
    1256           0 :         } else {
    1257             :             // Otherwise, add DISK_SNAPSHOT_PERIOD
    1258           0 :             next_snapshot_height = current_snapshot_height + DISK_SNAPSHOT_PERIOD;
    1259             :         }
    1260             : 
    1261           0 :         if (next_snapshot_height > stop_index->nHeight) {
    1262           0 :             break;
    1263             :         }
    1264             : 
    1265           0 :         const CBlockIndex* next_snapshot_index = stop_index->GetAncestor(next_snapshot_height);
    1266           0 :         if (!next_snapshot_index) {
    1267           0 :             break;
    1268             :         }
    1269             : 
    1270           0 :         snapshot_blocks.push_back(next_snapshot_index);
    1271           0 :         current_snapshot_height = next_snapshot_height;
    1272             :     }
    1273             : 
    1274           0 :     return snapshot_blocks;
    1275           0 : }
    1276             : 
    1277           0 : bool CDeterministicMNManager::VerifySnapshotPair(
    1278             :     const CBlockIndex* from_index, const CBlockIndex* to_index, const CDeterministicMNList& from_snapshot,
    1279             :     const CDeterministicMNList& to_snapshot, RecalcDiffsResult& result)
    1280             : {
    1281             :     // Verify this snapshot pair by applying all stored diffs sequentially
    1282           0 :     CDeterministicMNList test_list = from_snapshot;
    1283             : 
    1284             :     try {
    1285           0 :         for (int nHeight = from_index->nHeight + 1; nHeight <= to_index->nHeight; ++nHeight) {
    1286           0 :             const CBlockIndex* pIndex = to_index->GetAncestor(nHeight);
    1287           0 :             if (!pIndex) {
    1288           0 :                 result.verification_errors.push_back(strprintf("Failed to get ancestor at height %d", nHeight));
    1289           0 :                 return false;
    1290             :             }
    1291             : 
    1292           0 :             CDeterministicMNListDiff diff;
    1293           0 :             if (!m_evoDb.Read(std::make_pair(DB_LIST_DIFF, pIndex->GetBlockHash()), diff)) {
    1294           0 :                 result.verification_errors.push_back(strprintf("Failed to read diff at height %d", nHeight));
    1295           0 :                 return false;
    1296             :             }
    1297             : 
    1298           0 :             diff.nHeight = nHeight;
    1299           0 :             test_list.ApplyDiff(pIndex, diff);
    1300           0 :         }
    1301           0 :     } catch (const std::exception& e) {
    1302           0 :         result.verification_errors.push_back(strprintf("Exception during verification: %s", e.what()));
    1303           0 :         return false;
    1304           0 :     }
    1305             : 
    1306             :     // Verify that applying all diffs results in the target snapshot
    1307           0 :     bool is_snapshot_pair_valid = test_list.IsEqual(to_snapshot);
    1308             : 
    1309           0 :     if (is_snapshot_pair_valid) {
    1310           0 :         result.snapshots_verified++;
    1311           0 :     } else {
    1312           0 :         result.verification_errors.push_back(
    1313           0 :             strprintf("Verification failed between snapshots at heights %d and %d: "
    1314             :                       "Applied diffs do not match target snapshot",
    1315           0 :                       from_index->nHeight, to_index->nHeight));
    1316             :     }
    1317             : 
    1318           0 :     return is_snapshot_pair_valid;
    1319           0 : }
    1320             : 
    1321           0 : std::vector<std::pair<uint256, CDeterministicMNListDiff>> CDeterministicMNManager::RepairSnapshotPair(
    1322             :     const CBlockIndex* from_index, const CBlockIndex* to_index, const CDeterministicMNList& from_snapshot,
    1323             :     const CDeterministicMNList& to_snapshot, BuildListFromBlockFunc build_list_func, RecalcDiffsResult& result)
    1324             : {
    1325           0 :     CDeterministicMNList current_list = from_snapshot;
    1326             :     // Temporary storage for recalculated diffs (one per block in this snapshot interval)
    1327           0 :     std::vector<std::pair<uint256, CDeterministicMNListDiff>> temp_diffs;
    1328           0 :     temp_diffs.reserve(to_index->nHeight - from_index->nHeight);
    1329             : 
    1330           0 :     LogPrintf("CDeterministicMNManager::%s -- Repairing: recalculating diffs between snapshots at heights %d and %d\n",
    1331             :               __func__, from_index->nHeight, to_index->nHeight);
    1332             : 
    1333             :     try {
    1334           0 :         for (int nHeight = from_index->nHeight + 1; nHeight <= to_index->nHeight; ++nHeight) {
    1335           0 :             const CBlockIndex* pIndex = to_index->GetAncestor(nHeight);
    1336             : 
    1337             :             // Read the actual block from disk
    1338           0 :             CBlock block;
    1339           0 :             if (!node::ReadBlockFromDisk(block, pIndex, Params().GetConsensus())) {
    1340           0 :                 result.repair_errors.push_back(strprintf("CRITICAL: Failed to read block at height %d. "
    1341             :                     "Cannot repair - full reindex required.", nHeight));
    1342           0 :                 return {}; // Critical error - cannot continue repair
    1343             :             }
    1344             : 
    1345             :             // Use a dummy coins view to avoid UTXO lookups. At chain tip, coins from
    1346             :             // historical blocks may already be spent. Since these blocks were fully
    1347             :             // validated when originally connected, we don't need to re-verify coin
    1348             :             // availability - we only need to extract special transactions.
    1349           0 :             CCoinsView view_dummy;
    1350           0 :             CCoinsViewCache view(&view_dummy);
    1351             : 
    1352             :             // Build the new list by processing this block's special transactions
    1353             :             // Starting from current_list (our trusted state), not from corrupted diffs
    1354           0 :             CDeterministicMNList next_list;
    1355           0 :             BlockValidationState state;
    1356           0 :             if (!build_list_func(block, pIndex->pprev, current_list, view, false, state, next_list)) {
    1357           0 :                 result.repair_errors.push_back(
    1358           0 :                     strprintf("CRITICAL: Failed to build list for block at height %d: %s. "
    1359           0 :                               "Cannot repair - full reindex required.", nHeight, state.ToString()));
    1360           0 :                 return {}; // Critical error - cannot continue repair
    1361             :             }
    1362             : 
    1363             :             // Set the correct block hash
    1364           0 :             next_list.SetBlockHash(pIndex->GetBlockHash());
    1365             : 
    1366             :             // Calculate the diff between current and next
    1367           0 :             CDeterministicMNListDiff recalc_diff = current_list.BuildDiff(next_list);
    1368           0 :             recalc_diff.nHeight = nHeight;
    1369             :             // Store in temporary vector for this snapshot pair
    1370           0 :             temp_diffs.emplace_back(pIndex->GetBlockHash(), recalc_diff);
    1371             : 
    1372             :             // Move forward
    1373           0 :             current_list = next_list; // TODO: make CDeterministicMNList moveable
    1374           0 :         }
    1375             : 
    1376             :         // Verify that applying all diffs results in the target snapshot
    1377           0 :         if (current_list.IsEqual(to_snapshot)) {
    1378           0 :             LogPrintf("CDeterministicMNManager::%s -- Successfully recalculated %d diffs between heights %d and %d\n",
    1379             :                       __func__, temp_diffs.size(), from_index->nHeight, to_index->nHeight);
    1380           0 :             return temp_diffs; // Success - return recalculated diffs
    1381             :         } else {
    1382           0 :             result.repair_errors.push_back(
    1383           0 :                 strprintf("CRITICAL: Recalculation failed between snapshots at heights %d and %d: "
    1384             :                           "Applied diffs do not match target snapshot. Cannot repair - full reindex required.",
    1385           0 :                           from_index->nHeight, to_index->nHeight));
    1386           0 :             return {}; // Failed verification - return empty vector
    1387             :         }
    1388           0 :     } catch (const std::exception& e) {
    1389           0 :         result.repair_errors.push_back(strprintf("CRITICAL: Exception during recalculation: %s. "
    1390           0 :                                                   "Cannot repair - full reindex required.", e.what()));
    1391           0 :         return {}; // Exception - return empty vector
    1392           0 :     }
    1393           0 : }
    1394             : 
    1395           0 : void CDeterministicMNManager::WriteRepairedDiffs(
    1396             :     const std::vector<std::pair<uint256, CDeterministicMNListDiff>>& recalculated_diffs, RecalcDiffsResult& result)
    1397             : {
    1398           0 :     AssertLockNotHeld(cs);
    1399             : 
    1400           0 :     if (recalculated_diffs.empty()) {
    1401           0 :         return;
    1402             :     }
    1403             : 
    1404           0 :     CDBBatch batch(m_evoDb.GetRawDB());
    1405           0 :     const size_t BATCH_SIZE_THRESHOLD = 1 << 24; // 16MB
    1406           0 :     size_t diffs_written = 0;
    1407             : 
    1408           0 :     LogPrintf("CDeterministicMNManager::%s -- Writing %d repaired diffs to database...\n",
    1409             :               __func__, recalculated_diffs.size());
    1410             : 
    1411           0 :     for (const auto& [block_hash, diff] : recalculated_diffs) {
    1412           0 :         batch.Write(std::make_pair(DB_LIST_DIFF, block_hash), diff);
    1413           0 :         diffs_written++;
    1414             : 
    1415             :         // Write batch when it gets too large
    1416           0 :         if (batch.SizeEstimate() >= BATCH_SIZE_THRESHOLD) {
    1417           0 :             LogPrintf("CDeterministicMNManager::%s -- Flushing batch (%d diffs written so far)...\n",
    1418             :                       __func__, diffs_written);
    1419           0 :             m_evoDb.GetRawDB().WriteBatch(batch);
    1420           0 :             batch.Clear();
    1421           0 :         }
    1422             :     }
    1423             : 
    1424             :     // Write any remaining diffs in the batch
    1425           0 :     if (batch.SizeEstimate() > 0) {
    1426           0 :         LogPrintf("CDeterministicMNManager::%s -- Writing final batch...\n", __func__);
    1427           0 :         m_evoDb.GetRawDB().WriteBatch(batch);
    1428           0 :         batch.Clear();
    1429           0 :     }
    1430             : 
    1431             :     // Clear caches for repaired diffs so next read gets fresh data from disk
    1432             :     // Must clear both diff cache and list cache since lists were built from old diffs
    1433           0 :     LOCK(cs);
    1434           0 :     for (const auto& [block_hash, diff] : recalculated_diffs) {
    1435           0 :         mnListDiffsCache.erase(block_hash);
    1436           0 :         mnListsCache.erase(block_hash);
    1437             :     }
    1438             : 
    1439           0 :     LogPrintf("CDeterministicMNManager::%s -- Successfully repaired %d diffs (caches cleared)\n", __func__,
    1440             :               recalculated_diffs.size());
    1441           0 : }

Generated by: LCOV version 1.16