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 : }
|