LCOV - code coverage report
Current view: top level - src/evo - creditpool.cpp (source / functions) Hit Total Coverage
Test: test_dash_coverage.info Lines: 104 179 58.1 %
Date: 2026-06-25 07:23:51 Functions: 23 27 85.2 %

          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           0 : static bool GetDataFromUnlockTx(const CTransaction& tx, CAmount& toUnlock, uint64_t& index, TxValidationState& state)
      33             : {
      34           0 :     const auto opt_assetUnlockTx = GetTxPayload<CAssetUnlockPayload>(tx);
      35           0 :     if (!opt_assetUnlockTx.has_value()) {
      36           0 :         return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-payload");
      37             :     }
      38             : 
      39           0 :     index = opt_assetUnlockTx->getIndex();
      40           0 :     toUnlock = opt_assetUnlockTx->getFee();
      41           0 :     for (const CTxOut& txout : tx.vout) {
      42           0 :         if (!MoneyRange(txout.nValue)) {
      43           0 :             return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-txout-outofrange");
      44             :         }
      45           0 :         toUnlock += txout.nValue;
      46             :     }
      47           0 :     return true;
      48           0 : }
      49             : 
      50             : namespace {
      51       13923 : struct CreditPoolDataPerBlock {
      52       13923 :     CAmount credit_pool{0};
      53       13923 :     CAmount unlocked{0};
      54             :     std::unordered_set<uint64_t> indexes;
      55             : };
      56             : } // anonymous namespace
      57             : 
      58             : // it throws exception if anything went wrong
      59       15106 : 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       15106 :     if (!DeploymentActiveAt(*block_index, consensusParams, Consensus::DEPLOYMENT_DIP0003)) {
      64        1183 :         return std::nullopt;
      65             :     }
      66             : 
      67       13923 :     CreditPoolDataPerBlock blockData;
      68             : 
      69       13923 :     static Mutex cache_mutex;
      70       13926 :     static Uint256LruHashMap<CreditPoolDataPerBlock> block_data_cache GUARDED_BY(cache_mutex){
      71           3 :         static_cast<size_t>(Params().CreditPoolPeriodBlocks()) * 2};
      72       20411 :     if (LOCK(cache_mutex); block_data_cache.get(block_index->GetBlockHash(), blockData)) {
      73        6488 :         return blockData;
      74             :     }
      75             : 
      76        7435 :     CBlock block;
      77        7435 :     if (!ReadBlockFromDisk(block, block_index, consensusParams)) {
      78           0 :         throw std::runtime_error("failed-getcbforblock-read");
      79             :     }
      80             : 
      81        7435 :     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       14870 :     if (const auto opt_cbTx = GetTxPayload<CCbTx>(block.vtx[0]->vExtraPayload); opt_cbTx) {
      88        7435 :         blockData.credit_pool = opt_cbTx->creditPoolBalance;
      89        7435 :     } else {
      90           0 :         LogPrintf("%s: WARNING: No valid CbTx at height=%d\n", __func__, block_index->nHeight);
      91           0 :         return std::nullopt;
      92             :     }
      93       26512 :     for (const CTransactionRef& tx : block.vtx) {
      94       19077 :         if (!tx->IsSpecialTxVersion() || tx->nType != TRANSACTION_ASSET_UNLOCK) continue;
      95             : 
      96           0 :         CAmount unlocked{0};
      97           0 :         TxValidationState tx_state;
      98           0 :         uint64_t index{0};
      99           0 :         if (!GetDataFromUnlockTx(*tx, unlocked, index, tx_state)) {
     100           0 :             throw std::runtime_error(strprintf("%s: GetDataFromUnlockTx failed: %s", __func__, tx_state.ToString()));
     101             :         }
     102           0 :         blockData.unlocked += unlocked;
     103           0 :         blockData.indexes.insert(index);
     104           0 :     }
     105             : 
     106        7435 :     LOCK(cache_mutex);
     107        7435 :     block_data_cache.insert(block_index->GetBlockHash(), blockData);
     108        7435 :     return blockData;
     109       15106 : }
     110             : 
     111       38039 : std::string CCreditPool::ToString() const
     112             : {
     113       38039 :     return strprintf("CCreditPool(locked=%lld, currentLimit=%lld)",
     114       38039 :             locked, currentLimit);
     115             : }
     116             : 
     117       53223 : std::optional<CCreditPool> CCreditPoolManager::GetFromCache(const CBlockIndex& block_index)
     118             : {
     119       53223 :     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        7553 : void CCreditPoolManager::AddToCache(const uint256& block_hash, int height, const CCreditPool &pool)
     140             : {
     141             :     {
     142        7553 :         LOCK(cache_mutex);
     143        7553 :         creditPoolCache.insert(block_hash, pool);
     144        7553 :     }
     145        7553 :     if (height % DISK_SNAPSHOT_PERIOD == 0) {
     146          16 :         evoDb.Write(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool);
     147          16 :     }
     148        7553 : }
     149             : 
     150        7553 : CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null<const CBlockIndex*> block_index, CCreditPool prev)
     151             : {
     152        7553 :     std::optional<CreditPoolDataPerBlock> opt_block_data = GetCreditDataFromBlock(block_index, m_chainman.GetConsensus());
     153        7553 :     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        7553 :     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        7553 :     CRangesSet indexes{std::move(prev.indexes)};
     174        7553 :     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        7553 :     const CBlockIndex* distant_block_index{
     179        7553 :         block_index->GetAncestor(block_index->nHeight - m_chainman.GetParams().CreditPoolPeriodBlocks())};
     180        7553 :     CAmount distantUnlocked{0};
     181        7553 :     if (distant_block_index) {
     182        7553 :         if (std::optional<CreditPoolDataPerBlock> distant_block{
     183        7553 :                 GetCreditDataFromBlock(distant_block_index, m_chainman.GetConsensus())};
     184       13923 :             distant_block) {
     185        6370 :             distantUnlocked = distant_block->unlocked;
     186        6370 :         }
     187        7553 :     }
     188             : 
     189        7553 :     CAmount currentLimit = blockData.credit_pool;
     190        7553 :     const CAmount latelyUnlocked = prev.latelyUnlocked + blockData.unlocked - distantUnlocked;
     191        7553 :     if (DeploymentActiveAt(*block_index, m_chainman, Consensus::DEPLOYMENT_V24)) {
     192           0 :         currentLimit = std::max(CAmount(0), std::min(currentLimit, LimitAmountV24 - latelyUnlocked));
     193        7553 :     } else if (DeploymentActiveAt(*block_index, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_WITHDRAWALS)) {
     194        5650 :         currentLimit = std::min(currentLimit, LimitAmountV22);
     195        5650 :     } else {
     196             :         // Unlock limits in pre-v22 are max(100, min(.10 * assetlockpool, 1000)) inside window
     197        1903 :         if (currentLimit + latelyUnlocked > LimitAmountLow) {
     198         809 :             currentLimit = std::max(LimitAmountLow, blockData.credit_pool / 10) - latelyUnlocked;
     199         809 :             if (currentLimit < 0) currentLimit = 0;
     200         809 :         }
     201        1903 :         currentLimit = std::min(currentLimit, LimitAmountHigh - latelyUnlocked);
     202             :     }
     203             : 
     204        7553 :     if (currentLimit != 0 || latelyUnlocked > 0 || blockData.credit_pool > 0) {
     205        6553 :         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        6553 :     }
     211             : 
     212        7553 :     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        7553 :     CCreditPool pool{blockData.credit_pool, currentLimit, latelyUnlocked, indexes};
     218        7553 :     AddToCache(block_index->GetBlockHash(), block_index->nHeight, pool);
     219        7553 :     return pool;
     220             : 
     221        7553 : }
     222             : 
     223       45670 : CCreditPool CCreditPoolManager::GetCreditPool(const CBlockIndex* block_index)
     224             : {
     225       45670 :     std::stack<gsl::not_null<const CBlockIndex*>> to_calculate;
     226             : 
     227       45670 :     std::optional<CCreditPool> poolTmp;
     228       53223 :     while (block_index != nullptr && !(poolTmp = GetFromCache(*block_index)).has_value()) {
     229        7553 :         to_calculate.push(block_index);
     230        7553 :         block_index = block_index->pprev;
     231             :     }
     232       45670 :     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         360 : CCreditPoolManager::CCreditPoolManager(CEvoDB& _evoDb, const ChainstateManager& chainman) :
     241             :     evoDb{_evoDb},
     242             :     m_chainman{chainman}
     243         180 : {
     244         180 : }
     245             : 
     246         360 : CCreditPoolManager::~CCreditPoolManager() = default;
     247             : 
     248       91398 : CCreditPoolDiff::CCreditPoolDiff(CCreditPool starter, const CBlockIndex* pindexPrev,
     249             :                                  const Consensus::Params& consensusParams, const CAmount blockSubsidy) :
     250       30466 :     pool(std::move(starter))
     251       30466 : {
     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       30466 : }
     259             : 
     260           0 : bool CCreditPoolDiff::Lock(const CTransaction& tx, TxValidationState& state)
     261             : {
     262           0 :     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           0 :     for (const CTxOut& txout : tx.vout) {
     267           0 :         if (const CScript& script = txout.scriptPubKey; script.empty() || script[0] != OP_RETURN) continue;
     268             : 
     269           0 :         sessionLocked += txout.nValue;
     270           0 :         return true;
     271             :     }
     272             : 
     273           0 :     return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-lock-invalid");
     274           0 : }
     275             : 
     276           0 : bool CCreditPoolDiff::Unlock(const CTransaction& tx, TxValidationState& state)
     277             : {
     278           0 :     uint64_t index{0};
     279           0 :     CAmount toUnlock{0};
     280           0 :     if (!GetDataFromUnlockTx(tx, toUnlock, index, state)) {
     281             :         // state is set up inside GetDataFromUnlockTx
     282           0 :         return false;
     283             :     }
     284             : 
     285           0 :     if (sessionUnlocked + toUnlock > pool.currentLimit) {
     286           0 :         return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-too-much");
     287             :     }
     288             : 
     289           0 :     if (pool.indexes.Contains(index) || newIndexes.count(index)) {
     290           0 :         return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-duplicated-index");
     291             :     }
     292             : 
     293           0 :     newIndexes.insert(index);
     294           0 :     sessionUnlocked += toUnlock;
     295           0 :     return true;
     296           0 : }
     297             : 
     298       35490 : bool CCreditPoolDiff::ProcessLockUnlockTransaction(const CTransaction& tx, TxValidationState& state)
     299             : {
     300       35490 :     if (!tx.IsSpecialTxVersion()) return true;
     301             : 
     302             :     try {
     303       35487 :         switch (tx.nType) {
     304             :         case TRANSACTION_ASSET_LOCK:
     305           0 :             return Lock(tx, state);
     306             :         case TRANSACTION_ASSET_UNLOCK:
     307           0 :             return Unlock(tx, state);
     308             :         default:
     309       35487 :             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       35490 : }
     316             : 
     317       22835 : 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       22835 :         const CCreditPool creditPool = cpoolman.GetCreditPool(pindexPrev);
     323       22835 :         LogPrint(BCLog::CREDITPOOL, "%s: CCreditPool is %s\n", __func__, creditPool.ToString());
     324       22835 :         CCreditPoolDiff creditPoolDiff(creditPool, pindexPrev, consensusParams, blockSubsidy);
     325       58325 :         for (size_t i = 1; i < block.vtx.size(); ++i) {
     326       35490 :             const auto& tx = *block.vtx[i];
     327       35490 :             TxValidationState tx_state;
     328       35490 :             if (!creditPoolDiff.ProcessLockUnlockTransaction(tx, tx_state)) {
     329           0 :                 assert(tx_state.GetResult() == TxValidationResult::TX_CONSENSUS);
     330           0 :                 state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(),
     331           0 :                                  strprintf("Process Lock/Unlock Transaction failed at Credit Pool (tx hash %s) %s", tx.GetHash().ToString(), tx_state.GetDebugMessage()));
     332           0 :                 return std::nullopt;
     333             :             }
     334       35490 :         }
     335       22835 :         return creditPoolDiff;
     336       22835 :     } 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       22835 : }

Generated by: LCOV version 1.16