diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index df40c6378756..cafe65b023f4 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -17,12 +17,14 @@ #include #include #include +#include #include #include #include #include #include +#include using node::ReadBlockFromDisk; @@ -41,7 +43,11 @@ static bool GetDataFromUnlockTx(const CTransaction& tx, CAmount& toUnlock, uint6 if (!MoneyRange(txout.nValue)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-txout-outofrange"); } - toUnlock += txout.nValue; + const auto total = CheckedAdd(toUnlock, txout.nValue); + if (!total) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-overflow"); + } + toUnlock = *total; } return true; } @@ -288,6 +294,16 @@ CCreditPoolDiff::CCreditPoolDiff(CCreditPool starter, const CBlockIndex* pindexP } } +CAmount CCreditPoolDiff::GetTotalLocked() const +{ + // Subtract withdrawals first to preserve a representable net balance. + auto total = CheckedAdd(pool.locked, -sessionUnlocked); + if (total) total = CheckedAdd(*total, sessionLocked); + if (total) total = CheckedAdd(*total, platformReward); + if (!total) throw std::overflow_error("credit-pool balance overflow"); + return *total; +} + bool CCreditPoolDiff::Lock(const CTransaction& tx, TxValidationState& state) { if (const auto opt_assetLockTx = GetTxPayload(tx); !opt_assetLockTx) { @@ -297,7 +313,11 @@ bool CCreditPoolDiff::Lock(const CTransaction& tx, TxValidationState& state) for (const CTxOut& txout : tx.vout) { if (const CScript& script = txout.scriptPubKey; script.empty() || script[0] != OP_RETURN) continue; - sessionLocked += txout.nValue; + const auto total = CheckedAdd(sessionLocked, txout.nValue); + if (!total) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-lock-overflow"); + } + sessionLocked = *total; return true; } @@ -313,7 +333,8 @@ bool CCreditPoolDiff::Unlock(const CTransaction& tx, TxValidationState& state, s return false; } - if (sessionUnlocked + toUnlock > pool.currentLimit) { + const auto total = CheckedAdd(sessionUnlocked, toUnlock); + if (!total || *total > pool.currentLimit) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-too-much"); } @@ -323,7 +344,7 @@ bool CCreditPoolDiff::Unlock(const CTransaction& tx, TxValidationState& state, s newIndexes.insert(index); if (inserted_index) *inserted_index = index; - sessionUnlocked += toUnlock; + sessionUnlocked = *total; return true; } diff --git a/src/evo/creditpool.h b/src/evo/creditpool.h index bb3891a37403..e7676beace54 100644 --- a/src/evo/creditpool.h +++ b/src/evo/creditpool.h @@ -100,10 +100,9 @@ class CCreditPoolDiff { /** * this function returns total amount of credits for the next block + * Throws if the total cannot be represented as CAmount. */ - CAmount GetTotalLocked() const { - return pool.locked + sessionLocked - sessionUnlocked + platformReward; - } + CAmount GetTotalLocked() const; std::string ToString() const { return strprintf("CCreditPoolDiff(sessionLocked=%lld, sessionUnlocked=%lld, platforomReward=%lld, newIndexes=%lld, pool=%s)", sessionLocked, sessionUnlocked, platformReward, newIndexes.size(), pool.ToString()); diff --git a/src/test/evo_assetlocks_tests.cpp b/src/test/evo_assetlocks_tests.cpp index f9ac499106be..7bc4c09a5e85 100644 --- a/src/test/evo_assetlocks_tests.cpp +++ b/src/test/evo_assetlocks_tests.cpp @@ -25,6 +25,8 @@ #include +#include + // Helpers: static bool IsStandardTx(const CTransaction& tx, std::string& reason) { @@ -141,6 +143,17 @@ static CTransactionRef CreateCreditPoolUnlockTx(uint64_t index, CAmount amount, return MakeTransactionRef(std::move(tx)); } +static CTransactionRef CreateCreditPoolLockTx(CAmount amount) +{ + CMutableTransaction tx; + tx.nVersion = 3; + tx.nType = TRANSACTION_ASSET_LOCK; + tx.vout.emplace_back(amount, CScript{} << OP_RETURN << OP_0); + SetTxPayload(tx, CAssetLockPayload{std::vector{{amount, GetScriptForDestination(PKHash{})}}, + CAssetLockPayload::INITIAL_VERSION}); + return MakeTransactionRef(std::move(tx)); +} + BOOST_FIXTURE_TEST_SUITE(evo_assetlocks_tests, TestChain100Setup) static void CheckAssetLockCommon(uint8_t version, bool is_v24_active) @@ -885,6 +898,28 @@ BOOST_AUTO_TEST_CASE(credit_pool_unlock_limit_v24) BOOST_CHECK_EQUAL(limit(MAX_MONEY, MAX_MONEY), MAX_MONEY * 20 / 100); } +BOOST_FIXTURE_TEST_CASE(credit_pool_lock_arithmetic, TestChain100Setup) +{ + LOCK(cs_main); + const auto* tip = m_node.chainman->ActiveChain().Tip(); + CCreditPoolDiff diff{CCreditPool{}, tip, m_node.chainman->GetConsensus(), 0}; + const CAmount maximum = std::numeric_limits::max(); + const auto lock_max_money = CreateCreditPoolLockTx(MAX_MONEY); + for (CAmount i = 0; i < maximum / MAX_MONEY; ++i) { + TxValidationState state; + BOOST_REQUIRE(diff.ProcessLockUnlockTransaction(*lock_max_money, state)); + if (i == 1) BOOST_CHECK_EQUAL(diff.GetTotalLocked(), 2 * MAX_MONEY); + } + TxValidationState remainder_state; + BOOST_REQUIRE(diff.ProcessLockUnlockTransaction(*CreateCreditPoolLockTx(maximum % MAX_MONEY), remainder_state)); + BOOST_CHECK_EQUAL(diff.GetTotalLocked(), maximum); + + TxValidationState overflow_state; + BOOST_CHECK(!diff.ProcessLockUnlockTransaction(*CreateCreditPoolLockTx(1), overflow_state)); + BOOST_CHECK_EQUAL(overflow_state.GetRejectReason(), "failed-creditpool-lock-overflow"); + BOOST_CHECK_EQUAL(diff.GetTotalLocked(), maximum); +} + BOOST_FIXTURE_TEST_CASE(credit_pool_package_atomicity, TestChain100Setup) { LOCK(cs_main);