|
3 | 3 | // file COPYING or http://www.opensource.org/licenses/mit-license.php. |
4 | 4 |
|
5 | 5 | #include <test/util/setup_common.h> |
| 6 | +#include <test/util/txmempool.h> |
6 | 7 |
|
7 | 8 | #include <coinjoin/client.h> |
8 | 9 | #include <coinjoin/coinjoin.h> |
|
11 | 12 | #include <coinjoin/util.h> |
12 | 13 | #include <consensus/amount.h> |
13 | 14 | #include <interfaces/coinjoin.h> |
| 15 | +#include <masternode/sync.h> |
14 | 16 | #include <node/context.h> |
15 | 17 | #include <util/system.h> |
16 | 18 | #include <util/translation.h> |
17 | 19 | #include <policy/settings.h> |
| 20 | +#include <util/time.h> |
18 | 21 | #include <validation.h> |
| 22 | +#include <txmempool.h> |
19 | 23 | #include <wallet/context.h> |
20 | 24 | #include <wallet/spend.h> |
21 | 25 | #include <wallet/wallet.h> |
| 26 | +#include <wallet/walletdb.h> |
22 | 27 |
|
23 | 28 | #include <boost/test/unit_test.hpp> |
24 | 29 |
|
@@ -224,6 +229,120 @@ class CTransactionBuilderTestSetup : public TestChain100Setup |
224 | 229 | } |
225 | 230 | }; |
226 | 231 |
|
| 232 | +BOOST_FIXTURE_TEST_CASE(coinjoin_pending_observation_tests, CTransactionBuilderTestSetup) |
| 233 | +{ |
| 234 | + // 0.100001 DASH, a valid CoinJoin denomination |
| 235 | + constexpr CAmount nDenomAmount{10000100}; |
| 236 | + BOOST_REQUIRE(CoinJoin::IsDenominatedAmount(nDenomAmount)); |
| 237 | + CompactTallyItem tallyItem = GetTallyItem({nDenomAmount, nDenomAmount, nDenomAmount, nDenomAmount}); |
| 238 | + const COutPoint outpointUserLocked = tallyItem.outpoints[0]; |
| 239 | + const COutPoint outpointPending = tallyItem.outpoints[1]; |
| 240 | + const COutPoint outpointTimeout = tallyItem.outpoints[2]; |
| 241 | + const COutPoint outpointInMempool = tallyItem.outpoints[3]; |
| 242 | + |
| 243 | + // A denominated coin the user locked themselves, e.g. via `lockunspent` |
| 244 | + WITH_LOCK(wallet->cs_wallet, wallet->LockCoin(outpointUserLocked)); |
| 245 | + |
| 246 | + BOOST_CHECK(m_node.cj_walletman->doForClient("", [&](CCoinJoinClientManager& cj_man) { |
| 247 | + // A user-created lock is never adopted as a pending observation: it has no |
| 248 | + // CoinJoin record backing it, so it is left strictly alone |
| 249 | + cj_man.CheckPendingObservations(*m_node.mempool); |
| 250 | + BOOST_CHECK(!cj_man.IsPendingObservation(outpointUserLocked)); |
| 251 | + BOOST_CHECK_EQUAL(cj_man.GetPendingObservationCount(), 0); |
| 252 | + |
| 253 | + const int64_t nStart{GetTime()}; |
| 254 | + SetMockTime(nStart); |
| 255 | + cj_man.AddPendingObservation({outpointPending}); |
| 256 | + BOOST_CHECK(cj_man.IsPendingObservation(outpointPending)); |
| 257 | + BOOST_CHECK_EQUAL(cj_man.GetPendingObservationCount(), 1); |
| 258 | + BOOST_CHECK(WITH_LOCK(wallet->cs_wallet, return wallet->IsLockedCoin(outpointPending))); |
| 259 | + |
| 260 | + // Pending inputs are excluded from coin selection while unrelated inputs are not |
| 261 | + { |
| 262 | + LOCK(wallet->cs_wallet); |
| 263 | + bool fFoundPending{false}; |
| 264 | + bool fFoundFree{false}; |
| 265 | + for (const auto& out : AvailableCoinsListUnspent(*wallet).all()) { |
| 266 | + fFoundPending |= out.outpoint == outpointPending; |
| 267 | + fFoundFree |= out.outpoint == outpointTimeout; |
| 268 | + } |
| 269 | + BOOST_CHECK(!fFoundPending); |
| 270 | + BOOST_CHECK(fFoundFree); |
| 271 | + } |
| 272 | + |
| 273 | + // The pending set is mirrored to the wallet database so it survives a restart |
| 274 | + { |
| 275 | + std::map<COutPoint, int64_t> persisted; |
| 276 | + WalletBatch batch(wallet->GetDatabase()); |
| 277 | + BOOST_REQUIRE(batch.ReadCoinJoinPendingObs(persisted)); |
| 278 | + BOOST_CHECK_EQUAL(persisted.size(), 1); |
| 279 | + BOOST_CHECK(persisted.count(outpointPending) > 0); |
| 280 | + BOOST_CHECK_EQUAL(persisted.at(outpointPending), nStart); |
| 281 | + } |
| 282 | + |
| 283 | + // Nothing is released while the inputs remain unspent and the timeout has not passed |
| 284 | + cj_man.CheckPendingObservations(*m_node.mempool); |
| 285 | + BOOST_CHECK_EQUAL(cj_man.GetPendingObservationCount(), 1); |
| 286 | + |
| 287 | + // Once the wallet observes a transaction spending a pending input its lock is dropped |
| 288 | + CMutableTransaction mtxSpend; |
| 289 | + mtxSpend.vin.emplace_back(outpointPending); |
| 290 | + mtxSpend.vout.emplace_back(nDenomAmount - 1000, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
| 291 | + BOOST_REQUIRE(wallet->AddToWallet(MakeTransactionRef(mtxSpend), TxStateInMempool{})); |
| 292 | + cj_man.CheckPendingObservations(*m_node.mempool); |
| 293 | + BOOST_CHECK(!cj_man.IsPendingObservation(outpointPending)); |
| 294 | + BOOST_CHECK(!WITH_LOCK(wallet->cs_wallet, return wallet->IsLockedCoin(outpointPending))); |
| 295 | + BOOST_CHECK_EQUAL(cj_man.GetPendingObservationCount(), 0); |
| 296 | + |
| 297 | + // An input whose spending transaction sits in the mempool but has not reached the |
| 298 | + // wallet yet must NOT be released: findCoins() reports it as unspent (it only |
| 299 | + // knows outputs mempool transactions create, not the ones they spend), so the |
| 300 | + // mempool has to be consulted separately |
| 301 | + CMutableTransaction mtxMempool; |
| 302 | + mtxMempool.vin.emplace_back(outpointInMempool); |
| 303 | + mtxMempool.vout.emplace_back(nDenomAmount - 1000, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
| 304 | + { |
| 305 | + LOCK2(::cs_main, m_node.mempool->cs); |
| 306 | + m_node.mempool->addUnchecked(TestMemPoolEntryHelper().FromTx(MakeTransactionRef(mtxMempool))); |
| 307 | + } |
| 308 | + BOOST_REQUIRE(m_node.mempool->isSpent(outpointInMempool)); |
| 309 | + BOOST_REQUIRE(WITH_LOCK(wallet->cs_wallet, return wallet->GetWalletTx(mtxMempool.GetHash())) == nullptr); |
| 310 | + |
| 311 | + cj_man.AddPendingObservation({outpointTimeout, outpointInMempool}); |
| 312 | + BOOST_CHECK_EQUAL(cj_man.GetPendingObservationCount(), 2); |
| 313 | + SetMockTime(nStart + CCoinJoinClientManager::PENDING_OBSERVATION_TIMEOUT_SECONDS + 1); |
| 314 | + |
| 315 | + // The timeout never fires while the chain is still catching up: the spending |
| 316 | + // transaction could be sitting in a block we have not downloaded yet |
| 317 | + BOOST_REQUIRE(!m_node.mn_sync->IsBlockchainSynced()); |
| 318 | + cj_man.CheckPendingObservations(*m_node.mempool); |
| 319 | + BOOST_CHECK_EQUAL(cj_man.GetPendingObservationCount(), 2); |
| 320 | + BOOST_CHECK(WITH_LOCK(wallet->cs_wallet, return wallet->IsLockedCoin(outpointTimeout))); |
| 321 | + |
| 322 | + m_node.mn_sync->SwitchToNextAsset(); |
| 323 | + BOOST_REQUIRE(m_node.mn_sync->IsBlockchainSynced()); |
| 324 | + cj_man.CheckPendingObservations(*m_node.mempool); |
| 325 | + |
| 326 | + // Unspent everywhere - released (with a warning) after the terminal timeout |
| 327 | + BOOST_CHECK(!cj_man.IsPendingObservation(outpointTimeout)); |
| 328 | + BOOST_CHECK(!WITH_LOCK(wallet->cs_wallet, return wallet->IsLockedCoin(outpointTimeout))); |
| 329 | + // Spent by an unconfirmed transaction the wallet has not recorded - kept locked |
| 330 | + BOOST_CHECK(cj_man.IsPendingObservation(outpointInMempool)); |
| 331 | + BOOST_CHECK(WITH_LOCK(wallet->cs_wallet, return wallet->IsLockedCoin(outpointInMempool))); |
| 332 | + BOOST_CHECK_EQUAL(cj_man.GetPendingObservationCount(), 1); |
| 333 | + |
| 334 | + // A manual unlock (e.g. via lockunspent) purges the pending entry |
| 335 | + WITH_LOCK(wallet->cs_wallet, wallet->UnlockCoin(outpointInMempool)); |
| 336 | + cj_man.CheckPendingObservations(*m_node.mempool); |
| 337 | + BOOST_CHECK(!cj_man.IsPendingObservation(outpointInMempool)); |
| 338 | + BOOST_CHECK_EQUAL(cj_man.GetPendingObservationCount(), 0); |
| 339 | + |
| 340 | + // The user's own lock was never touched throughout |
| 341 | + BOOST_CHECK(WITH_LOCK(wallet->cs_wallet, return wallet->IsLockedCoin(outpointUserLocked))); |
| 342 | + SetMockTime(0); |
| 343 | + })); |
| 344 | +} |
| 345 | + |
227 | 346 | BOOST_FIXTURE_TEST_CASE(coinjoin_manager_start_stop_tests, CTransactionBuilderTestSetup) |
228 | 347 | { |
229 | 348 | BOOST_CHECK(m_node.cj_walletman->doForClient("", [](auto& cj_man) { |
|
0 commit comments