LCOV - code coverage report
Current view: top level - src/evo - creditpool.cpp (source / functions) Hit Total Coverage
Test: total_coverage.info Lines: 146 179 81.6 %
Date: 2026-06-25 07:23:43 Functions: 27 27 100.0 %

          Line data    Source code
       1             : // Copyright (c) 2023-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/creditpool.h>
       6             : 
       7             : #include <evo/assetlocktx.h>
       8             : #include <evo/cbtx.h>
       9             : #include <evo/evodb.h>
      10             : #include <evo/specialtx.h>
      11             : 
      12             : #include <chain.h>
      13             : #include <chainparams.h>
      14             : #include <consensus/validation.h>
      15             : #include <deploymentstatus.h>
      16             : #include <logging.h>
      17             : #include <node/blockstorage.h>
      18             : #include <validation.h>
      19             : 
      20             : #include <algorithm>
      21             : #include <exception>
      22             : #include <memory>
      23             : #include <stack>
      24             : 
      25             : using node::ReadBlockFromDisk;
      26             : 
      27             : // Forward declaration to prevent a new circular dependencies through masternode/payments.h
      28             : CAmount PlatformShare(const CAmount masternodeReward);
      29             : 
      30             : static const std::string DB_CREDITPOOL_SNAPSHOT = "cpm_S";
      31             : 
      32        1050 : static bool GetDataFromUnlockTx(const CTransaction& tx, CAmount& toUnlock, uint64_t& index, TxValidationState& state)
      33             : {
      34        1050 :     const auto opt_assetUnlockTx = GetTxPayload<CAssetUnlockPayload>(tx);
      35        1050 :     if (!opt_assetUnlockTx.has_value()) {
      36           0 :         return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-payload");
      37             :     }
      38             : 
      39        1050 :     index = opt_assetUnlockTx->getIndex();
      40        1050 :     toUnlock = opt_assetUnlockTx->getFee();
      41        2100 :     for (const CTxOut& txout : tx.vout) {
      42        1050 :         if (!MoneyRange(txout.nValue)) {
      43           0 :             return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-txout-outofrange");
      44             :         }
      45        1050 :         toUnlock += txout.nValue;
      46             :     }
      47        1050 :     return true;
      48        1050 : }
      49             : 
      50             : namespace {
      51      255967 : struct CreditPoolDataPerBlock {
      52      255967 :     CAmount credit_pool{0};
      53      255967 :     CAmount unlocked{0};
      54             :     std::unordered_set<uint64_t> indexes;
      55             : };
      56             : } // anonymous namespace
      57             : 
      58             : // it throws exception if anything went wrong
      59      260410 : static std::optional<CreditPoolDataPerBlock> GetCreditDataFromBlock(const gsl::not_null<const CBlockIndex*> block_index,
      60             :                                                                     const Consensus::Params& consensusParams)
      61             : {
      62             :     // There's no CbTx before DIP0003 activation
      63      260410 :     if (!DeploymentActiveAt(*block_index, consensusParams, Consensus::DEPLOYMENT_DIP0003)) {
      64        4443 :         return std::nullopt;
      65             :     }
      66             : 
      67      255967 :     CreditPoolDataPerBlock blockData;
      68             : 
      69      255967 :     static Mutex cache_mutex;
      70      256696 :     static Uint256LruHashMap<CreditPoolDataPerBlock> block_data_cache GUARDED_BY(cache_mutex){
      71         729 :         static_cast<size_t>(Params().CreditPoolPeriodBlocks()) * 2};
      72      323254 :     if (LOCK(cache_mutex); block_data_cache.get(block_index->GetBlockHash(), blockData)) {
      73       67287 :         return blockData;
      74             :     }
      75             : 
      76      188680 :     CBlock block;
      77      188680 :     if (!ReadBlockFromDisk(block, block_index, consensusParams)) {
      78           0 :         throw std::runtime_error("failed-getcbforblock-read");
      79             :     }
      80             : 
      81      188680 :     if (block.vtx.empty() || block.vtx[0]->vExtraPayload.empty() || !block.vtx[0]->IsSpecialTxVersion()) {
      82           0 :         LogPrintf("%s: ERROR: empty CbTx for CreditPool at height=%d\n", __func__, block_index->nHeight);
      83           0 :         return std::nullopt;
      84             :     }
      85             : 
      86             : 
      87      377360 :     if (const auto opt_cbTx = GetTxPayload<CCbTx>(block.vtx[0]->vExtraPayload); opt_cbTx) {
      88      188680 :         blockData.credit_pool = opt_cbTx->creditPoolBalance;
      89      188680 :     } else {
      90           0 :         LogPrintf("%s: WARNING: No valid CbTx at height=%d\n", __func__, block_index->nHeight);
      91           0 :         return std::nullopt;
      92             :     }
      93      665553 :     for (const CTransactionRef& tx : block.vtx) {
      94      476873 :         if (!tx->IsSpecialTxVersion() || tx->nType != TRANSACTION_ASSET_UNLOCK) continue;
      95             : 
      96         156 :         CAmount unlocked{0};
      97         156 :         TxValidationState tx_state;
      98         156 :         uint64_t index{0};
      99         156 :         if (!GetDataFromUnlockTx(*tx, unlocked, index, tx_state)) {
     100           0 :             throw std::runtime_error(strprintf("%s: GetDataFromUnlockTx failed: %s", __func__, tx_state.ToString()));
     101             :         }
     102         156 :         blockData.unlocked += unlocked;
     103         156 :         blockData.indexes.insert(index);
     104         156 :     }
     105             : 
     106      188680 :     LOCK(cache_mutex);
     107      188680 :     block_data_cache.insert(block_index->GetBlockHash(), blockData);
     108      188680 :     return blockData;
     109      260410 : }
     110             : 
     111      246911 : std::string CCreditPool::ToString() const
     112             : {
     113      246911 :     return strprintf("CCreditPool(locked=%lld, currentLimit=%lld)",
     114      246911 :             locked, currentLimit);
     115             : }
     116             : 
     117      401001 : std::optional<CCreditPool> CCreditPoolManager::GetFromCache(const CBlockIndex& block_index)
     118             : {
     119      401001 :     if (!DeploymentActiveAt(block_index, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_V20)) return CCreditPool{};
     120             : 
     121             :     const uint256 block_hash = block_index.GetBlockHash();
     122             :     CCreditPool pool;
     123             :     {
     124             :         LOCK(cache_mutex);
     125             :         if (creditPoolCache.get(block_hash, pool)) {
     126             :             return pool;
     127             :         }
     128             :     }
     129             :     if (block_index.nHeight % DISK_SNAPSHOT_PERIOD == 0) {
     130             :         if (evoDb.Read(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool)) {
     131             :             LOCK(cache_mutex);
     132             :             creditPoolCache.insert(block_hash, pool);
     133             :             return pool;
     134             :         }
     135             :     }
     136             :     return std::nullopt;
     137             : }
     138             : 
     139      130205 : void CCreditPoolManager::AddToCache(const uint256& block_hash, int height, const CCreditPool &pool)
     140             : {
     141             :     {
     142      130205 :         LOCK(cache_mutex);
     143      130205 :         creditPoolCache.insert(block_hash, pool);
     144      130205 :     }
     145      130205 :     if (height % DISK_SNAPSHOT_PERIOD == 0) {
     146          54 :         evoDb.Write(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool);
     147          54 :     }
     148      130205 : }
     149             : 
     150      130205 : CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null<const CBlockIndex*> block_index, CCreditPool prev)
     151             : {
     152      130205 :     std::optional<CreditPoolDataPerBlock> opt_block_data = GetCreditDataFromBlock(block_index, m_chainman.GetConsensus());
     153      130205 :     if (!opt_block_data) {
     154             :         // If reading of previous block is not successfully, but
     155             :         // prev contains credit pool related data, something strange happened
     156           0 :         if (prev.locked != 0) {
     157           0 :             throw std::runtime_error(strprintf("Failed to create CreditPool but previous block has value"));
     158             :         }
     159           0 :         if (!prev.indexes.IsEmpty()) {
     160           0 :             throw std::runtime_error(
     161           0 :                 strprintf("Failed to create CreditPool but asset unlock transactions already mined"));
     162             :         }
     163           0 :         CCreditPool emptyPool;
     164           0 :         AddToCache(block_index->GetBlockHash(), block_index->nHeight, emptyPool);
     165           0 :         return emptyPool;
     166           0 :     }
     167      130205 :     const CreditPoolDataPerBlock& blockData{*opt_block_data};
     168             : 
     169             :     // We use here sliding window with Params().CreditPoolPeriodBlocks to determine
     170             :     // current limits for asset unlock transactions.
     171             :     // Indexes should not be duplicated since genesis block, but the Unlock Amount
     172             :     // of withdrawal transaction is limited only by this window
     173      130205 :     CRangesSet indexes{std::move(prev.indexes)};
     174      130358 :     if (std::any_of(blockData.indexes.begin(), blockData.indexes.end(), [&](const uint64_t index) { return !indexes.Add(index); })) {
     175           0 :         throw std::runtime_error(strprintf("%s: failed-getcreditpool-index-duplicated", __func__));
     176             :     }
     177             : 
     178      130205 :     const CBlockIndex* distant_block_index{
     179      130205 :         block_index->GetAncestor(block_index->nHeight - m_chainman.GetParams().CreditPoolPeriodBlocks())};
     180      130205 :     CAmount distantUnlocked{0};
     181      130205 :     if (distant_block_index) {
     182      130205 :         if (std::optional<CreditPoolDataPerBlock> distant_block{
     183      130205 :                 GetCreditDataFromBlock(distant_block_index, m_chainman.GetConsensus())};
     184      255967 :             distant_block) {
     185      125762 :             distantUnlocked = distant_block->unlocked;
     186      125762 :         }
     187      130205 :     }
     188             : 
     189      130205 :     CAmount currentLimit = blockData.credit_pool;
     190      130205 :     const CAmount latelyUnlocked = prev.latelyUnlocked + blockData.unlocked - distantUnlocked;
     191      130205 :     if (DeploymentActiveAt(*block_index, m_chainman, Consensus::DEPLOYMENT_V24)) {
     192         987 :         currentLimit = std::max(CAmount(0), std::min(currentLimit, LimitAmountV24 - latelyUnlocked));
     193      130205 :     } else if (DeploymentActiveAt(*block_index, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_WITHDRAWALS)) {
     194       14122 :         currentLimit = std::min(currentLimit, LimitAmountV22);
     195       14122 :     } else {
     196             :         // Unlock limits in pre-v22 are max(100, min(.10 * assetlockpool, 1000)) inside window
     197      115096 :         if (currentLimit + latelyUnlocked > LimitAmountLow) {
     198       60477 :             currentLimit = std::max(LimitAmountLow, blockData.credit_pool / 10) - latelyUnlocked;
     199       60477 :             if (currentLimit < 0) currentLimit = 0;
     200       60477 :         }
     201      115096 :         currentLimit = std::min(currentLimit, LimitAmountHigh - latelyUnlocked);
     202             :     }
     203             : 
     204      130205 :     if (currentLimit != 0 || latelyUnlocked > 0 || blockData.credit_pool > 0) {
     205      125065 :         LogPrint(BCLog::CREDITPOOL, /* Continued */
     206             :                  "CCreditPoolManager: asset unlock limits on height: %d locked: %d.%08d limit: %d.%08d "
     207             :                  "unlocked-in-window: %d.%08d\n",
     208             :                  block_index->nHeight, blockData.credit_pool / COIN, blockData.credit_pool % COIN, currentLimit / COIN,
     209             :                  currentLimit % COIN, latelyUnlocked / COIN, latelyUnlocked % COIN);
     210      125065 :     }
     211             : 
     212      130205 :     if (currentLimit < 0) {
     213           0 :         throw std::runtime_error(
     214           0 :             strprintf("Negative limit for CreditPool: %d.%08d\n", currentLimit / COIN, currentLimit % COIN));
     215             :     }
     216             : 
     217      130205 :     CCreditPool pool{blockData.credit_pool, currentLimit, latelyUnlocked, indexes};
     218      130205 :     AddToCache(block_index->GetBlockHash(), block_index->nHeight, pool);
     219      130205 :     return pool;
     220             : 
     221      130205 : }
     222             : 
     223      270796 : CCreditPool CCreditPoolManager::GetCreditPool(const CBlockIndex* block_index)
     224             : {
     225      270796 :     std::stack<gsl::not_null<const CBlockIndex*>> to_calculate;
     226             : 
     227      270796 :     std::optional<CCreditPool> poolTmp;
     228      401001 :     while (block_index != nullptr && !(poolTmp = GetFromCache(*block_index)).has_value()) {
     229      130205 :         to_calculate.push(block_index);
     230      130205 :         block_index = block_index->pprev;
     231             :     }
     232      270796 :     if (block_index == nullptr) poolTmp = CCreditPool{};
     233             :     while (!to_calculate.empty()) {
     234             :         poolTmp = ConstructCreditPool(to_calculate.top(), *poolTmp);
     235             :         to_calculate.pop();
     236             :     }
     237             :     return *poolTmp;
     238           0 : }
     239             : 
     240        6126 : CCreditPoolManager::CCreditPoolManager(CEvoDB& _evoDb, const ChainstateManager& chainman) :
     241             :     evoDb{_evoDb},
     242             :     m_chainman{chainman}
     243        3063 : {
     244        3063 : }
     245             : 
     246        6126 : CCreditPoolManager::~CCreditPoolManager() = default;
     247             : 
     248      477768 : CCreditPoolDiff::CCreditPoolDiff(CCreditPool starter, const CBlockIndex* pindexPrev,
     249             :                                  const Consensus::Params& consensusParams, const CAmount blockSubsidy) :
     250      159256 :     pool(std::move(starter))
     251      159256 : {
     252             :     assert(pindexPrev);
     253             : 
     254             :     if (DeploymentActiveAfter(pindexPrev, consensusParams, Consensus::DEPLOYMENT_MN_RR)) {
     255             :         // If credit pool exists, it means v20 is activated
     256             :         platformReward = PlatformShare(GetMasternodePayment(pindexPrev->nHeight + 1, blockSubsidy, /*fV20Active=*/ true));
     257             :     }
     258      159256 : }
     259             : 
     260        2336 : bool CCreditPoolDiff::Lock(const CTransaction& tx, TxValidationState& state)
     261             : {
     262        2336 :     if (const auto opt_assetLockTx = GetTxPayload<CAssetLockPayload>(tx); !opt_assetLockTx) {
     263           0 :         return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-lock-payload");
     264             :     }
     265             : 
     266        2614 :     for (const CTxOut& txout : tx.vout) {
     267        2614 :         if (const CScript& script = txout.scriptPubKey; script.empty() || script[0] != OP_RETURN) continue;
     268             : 
     269        2336 :         sessionLocked += txout.nValue;
     270        2336 :         return true;
     271             :     }
     272             : 
     273           0 :     return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-lock-invalid");
     274        2336 : }
     275             : 
     276         894 : bool CCreditPoolDiff::Unlock(const CTransaction& tx, TxValidationState& state)
     277             : {
     278         894 :     uint64_t index{0};
     279         894 :     CAmount toUnlock{0};
     280         894 :     if (!GetDataFromUnlockTx(tx, toUnlock, index, state)) {
     281             :         // state is set up inside GetDataFromUnlockTx
     282           0 :         return false;
     283             :     }
     284             : 
     285         894 :     if (sessionUnlocked + toUnlock > pool.currentLimit) {
     286         618 :         return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-too-much");
     287             :     }
     288             : 
     289         276 :     if (pool.indexes.Contains(index) || newIndexes.count(index)) {
     290           0 :         return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-duplicated-index");
     291             :     }
     292             : 
     293         276 :     newIndexes.insert(index);
     294         276 :     sessionUnlocked += toUnlock;
     295         276 :     return true;
     296         894 : }
     297             : 
     298      205582 : bool CCreditPoolDiff::ProcessLockUnlockTransaction(const CTransaction& tx, TxValidationState& state)
     299             : {
     300      205582 :     if (!tx.IsSpecialTxVersion()) return true;
     301             : 
     302             :     try {
     303      201521 :         switch (tx.nType) {
     304             :         case TRANSACTION_ASSET_LOCK:
     305        2336 :             return Lock(tx, state);
     306             :         case TRANSACTION_ASSET_UNLOCK:
     307         894 :             return Unlock(tx, state);
     308             :         default:
     309      198291 :             return true;
     310             :         }
     311           0 :     } catch (const std::exception& e) {
     312           0 :         LogPrintf("%s -- failed: %s\n", __func__, e.what());
     313           0 :         return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-procassetlocksinblock");
     314           0 :     }
     315      205582 : }
     316             : 
     317      135383 : std::optional<CCreditPoolDiff> GetCreditPoolDiffForBlock(CCreditPoolManager& cpoolman,
     318             :                                                          const CBlock& block, const CBlockIndex* pindexPrev, const Consensus::Params& consensusParams,
     319             :                                                          const CAmount blockSubsidy, BlockValidationState& state)
     320             : {
     321             :     try {
     322      135383 :         const CCreditPool creditPool = cpoolman.GetCreditPool(pindexPrev);
     323      135383 :         LogPrint(BCLog::CREDITPOOL, "%s: CCreditPool is %s\n", __func__, creditPool.ToString());
     324      135383 :         CCreditPoolDiff creditPoolDiff(creditPool, pindexPrev, consensusParams, blockSubsidy);
     325      338713 :         for (size_t i = 1; i < block.vtx.size(); ++i) {
     326      203342 :             const auto& tx = *block.vtx[i];
     327      203342 :             TxValidationState tx_state;
     328      203342 :             if (!creditPoolDiff.ProcessLockUnlockTransaction(tx, tx_state)) {
     329          12 :                 assert(tx_state.GetResult() == TxValidationResult::TX_CONSENSUS);
     330          24 :                 state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(),
     331          12 :                                  strprintf("Process Lock/Unlock Transaction failed at Credit Pool (tx hash %s) %s", tx.GetHash().ToString(), tx_state.GetDebugMessage()));
     332          12 :                 return std::nullopt;
     333             :             }
     334      203342 :         }
     335      135371 :         return creditPoolDiff;
     336      135383 :     } catch (const std::exception& e) {
     337           0 :         LogPrintf("%s -- failed: %s\n", __func__, e.what());
     338           0 :         state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-getcreditpooldiff");
     339           0 :         return std::nullopt;
     340           0 :     }
     341      135383 : }

Generated by: LCOV version 1.16