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