group_outputs_tests.cpp raw

   1  // Copyright (c) 2022 The Limenka developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or https://www.opensource.org/licenses/mit-license.php.
   4  
   5  #include <test/util/setup_common.h>
   6  
   7  #include <wallet/coinselection.h>
   8  #include <wallet/spend.h>
   9  #include <wallet/test/util.h>
  10  #include <wallet/wallet.h>
  11  
  12  #include <boost/test/unit_test.hpp>
  13  
  14  namespace wallet {
  15  BOOST_FIXTURE_TEST_SUITE(group_outputs_tests, TestingSetup)
  16  
  17  static int nextLockTime = 0;
  18  
  19  static std::shared_ptr<CWallet> NewWallet(const node::NodeContext& m_node)
  20  {
  21      std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase());
  22      wallet->LoadWallet();
  23      LOCK(wallet->cs_wallet);
  24      wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
  25      wallet->SetupDescriptorScriptPubKeyMans();
  26      return wallet;
  27  }
  28  
  29  static void addCoin(CoinsResult& coins,
  30                       CWallet& wallet,
  31                       const CTxDestination& dest,
  32                       const CAmount& nValue,
  33                       bool is_from_me,
  34                       CFeeRate fee_rate = CFeeRate(0),
  35                       int depth = 6)
  36  {
  37      CMutableTransaction tx;
  38      tx.nLockTime = nextLockTime++;        // so all transactions get different hashes
  39      tx.vout.resize(1);
  40      tx.vout[0].nValue = nValue;
  41      tx.vout[0].scriptPubKey = GetScriptForDestination(dest);
  42  
  43      const auto txid{tx.GetHash().ToUint256()};
  44      LOCK(wallet.cs_wallet);
  45      auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{}));
  46      assert(ret.second);
  47      CWalletTx& wtx = (*ret.first).second;
  48      const auto& txout = wtx.tx->vout.at(0);
  49      coins.Add(*Assert(OutputTypeFromDestination(dest)),
  50                {COutPoint(wtx.GetHash(), 0),
  51                     txout,
  52                     depth,
  53                     CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr),
  54                     /*spendable=*/ true,
  55                     /*solvable=*/ true,
  56                     /*safe=*/ true,
  57                     wtx.GetTxTime(),
  58                     is_from_me,
  59                     fee_rate});
  60  }
  61  
  62   CoinSelectionParams makeSelectionParams(FastRandomContext& rand, bool avoid_partial_spends)
  63  {
  64      return CoinSelectionParams{
  65              rand,
  66              /*change_output_size=*/ 0,
  67              /*change_spend_size=*/ 0,
  68              /*min_change_target=*/ CENT,
  69              /*effective_feerate=*/ CFeeRate(0),
  70              /*long_term_feerate=*/ CFeeRate(0),
  71              /*discard_feerate=*/ CFeeRate(0),
  72              /*tx_noinputs_size=*/ 0,
  73              /*avoid_partial=*/ avoid_partial_spends,
  74      };
  75  }
  76  
  77  class GroupVerifier
  78  {
  79  public:
  80      std::shared_ptr<CWallet> wallet{nullptr};
  81      CoinsResult coins_pool;
  82      FastRandomContext rand;
  83  
  84      void GroupVerify(const OutputType type,
  85                       const CoinEligibilityFilter& filter,
  86                       bool avoid_partial_spends,
  87                       bool positive_only,
  88                       int expected_size)
  89      {
  90          OutputGroupTypeMap groups = GroupOutputs(*wallet, coins_pool, makeSelectionParams(rand, avoid_partial_spends), {{filter}})[filter];
  91          std::vector<OutputGroup>& groups_out = positive_only ? groups.groups_by_type[type].positive_group :
  92                                                 groups.groups_by_type[type].mixed_group;
  93          BOOST_CHECK_EQUAL(groups_out.size(), expected_size);
  94      }
  95  
  96      void GroupAndVerify(const OutputType type,
  97                          const CoinEligibilityFilter& filter,
  98                          int expected_with_partial_spends_size,
  99                          int expected_without_partial_spends_size,
 100                          bool positive_only)
 101      {
 102          // First avoid partial spends
 103          GroupVerify(type, filter, /*avoid_partial_spends=*/false, positive_only,  expected_with_partial_spends_size);
 104          // Second don't avoid partial spends
 105          GroupVerify(type, filter, /*avoid_partial_spends=*/true, positive_only, expected_without_partial_spends_size);
 106      }
 107  };
 108  
 109  BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
 110  {
 111      const auto& wallet = NewWallet(m_node);
 112      GroupVerifier group_verifier;
 113      group_verifier.wallet = wallet;
 114  
 115      const CoinEligibilityFilter& BASIC_FILTER{1, 6, 0};
 116  
 117      // #################################################################################
 118      // 10 outputs from different txs going to the same script
 119      // 1) if partial spends is enabled --> must not be grouped
 120      // 2) if partial spends is not enabled --> must be grouped into a single OutputGroup
 121      // #################################################################################
 122  
 123      unsigned long GROUP_SIZE = 10;
 124      const CTxDestination dest = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
 125      for (unsigned long i = 0; i < GROUP_SIZE; i++) {
 126          addCoin(group_verifier.coins_pool, *wallet, dest, 10 * COIN, /*is_from_me=*/true);
 127      }
 128  
 129      group_verifier.GroupAndVerify(OutputType::BECH32,
 130                                    BASIC_FILTER,
 131                                    /*expected_with_partial_spends_size=*/ GROUP_SIZE,
 132                                    /*expected_without_partial_spends_size=*/ 1,
 133                                    /*positive_only=*/ true);
 134  
 135      // ####################################################################################
 136      // 3) 10 more UTXO are added with a different script --> must be grouped into a single
 137      //    group for avoid partial spends and 10 different output groups for partial spends
 138      // ####################################################################################
 139  
 140      const CTxDestination dest2 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
 141      for (unsigned long i = 0; i < GROUP_SIZE; i++) {
 142          addCoin(group_verifier.coins_pool, *wallet, dest2, 5 * COIN, /*is_from_me=*/true);
 143      }
 144  
 145      group_verifier.GroupAndVerify(OutputType::BECH32,
 146              BASIC_FILTER,
 147              /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,
 148              /*expected_without_partial_spends_size=*/ 2,
 149              /*positive_only=*/ true);
 150  
 151      // ################################################################################
 152      // 4) Now add a negative output --> which will be skipped if "positive_only" is set
 153      // ################################################################################
 154  
 155      const CTxDestination dest3 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
 156      addCoin(group_verifier.coins_pool, *wallet, dest3, 1, true, CFeeRate(100));
 157      BOOST_CHECK(group_verifier.coins_pool.coins[OutputType::BECH32].back().GetEffectiveValue() <= 0);
 158  
 159      // First expect no changes with "positive_only" enabled
 160      group_verifier.GroupAndVerify(OutputType::BECH32,
 161              BASIC_FILTER,
 162              /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,
 163              /*expected_without_partial_spends_size=*/ 2,
 164              /*positive_only=*/ true);
 165  
 166      // Then expect changes with "positive_only" disabled
 167      group_verifier.GroupAndVerify(OutputType::BECH32,
 168              BASIC_FILTER,
 169              /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
 170              /*expected_without_partial_spends_size=*/ 3,
 171              /*positive_only=*/ false);
 172  
 173  
 174      // ##############################################################################
 175      // 5) Try to add a non-eligible UTXO (due not fulfilling the min depth target for
 176      //    "not mine" UTXOs) --> it must not be added to any group
 177      // ##############################################################################
 178  
 179      const CTxDestination dest4 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
 180      addCoin(group_verifier.coins_pool, *wallet, dest4, 6 * COIN,
 181              /*is_from_me=*/false, CFeeRate(0), /*depth=*/5);
 182  
 183      // Expect no changes from this round and the previous one (point 4)
 184      group_verifier.GroupAndVerify(OutputType::BECH32,
 185              BASIC_FILTER,
 186              /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
 187              /*expected_without_partial_spends_size=*/ 3,
 188              /*positive_only=*/ false);
 189  
 190  
 191      // ##############################################################################
 192      // 6) Try to add a non-eligible UTXO (due not fulfilling the min depth target for
 193      //    "mine" UTXOs) --> it must not be added to any group
 194      // ##############################################################################
 195  
 196      const CTxDestination dest5 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
 197      addCoin(group_verifier.coins_pool, *wallet, dest5, 6 * COIN,
 198              /*is_from_me=*/true, CFeeRate(0), /*depth=*/0);
 199  
 200      // Expect no changes from this round and the previous one (point 5)
 201      group_verifier.GroupAndVerify(OutputType::BECH32,
 202              BASIC_FILTER,
 203              /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
 204              /*expected_without_partial_spends_size=*/ 3,
 205              /*positive_only=*/ false);
 206  
 207      // ###########################################################################################
 208      // 7) Surpass the OUTPUT_GROUP_MAX_ENTRIES and verify that a second partial group gets created
 209      // ###########################################################################################
 210  
 211      const CTxDestination dest7 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
 212      uint16_t NUM_SINGLE_ENTRIES = 101;
 213      for (unsigned long i = 0; i < NUM_SINGLE_ENTRIES; i++) { // OUTPUT_GROUP_MAX_ENTRIES{100}
 214          addCoin(group_verifier.coins_pool, *wallet, dest7, 9 * COIN, /*is_from_me=*/true);
 215      }
 216  
 217      // Exclude partial groups only adds one more group to the previous test case (point 6)
 218      int PREVIOUS_ROUND_COUNT = GROUP_SIZE * 2 + 1;
 219      group_verifier.GroupAndVerify(OutputType::BECH32,
 220              BASIC_FILTER,
 221              /*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,
 222              /*expected_without_partial_spends_size=*/ 4,
 223              /*positive_only=*/ false);
 224  
 225      // Include partial groups should add one more group inside the "avoid partial spends" count
 226      const CoinEligibilityFilter& avoid_partial_groups_filter{1, 6, 0, 0, /*include_partial=*/ true};
 227      group_verifier.GroupAndVerify(OutputType::BECH32,
 228              avoid_partial_groups_filter,
 229              /*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,
 230              /*expected_without_partial_spends_size=*/ 5,
 231              /*positive_only=*/ false);
 232  }
 233  
 234  BOOST_AUTO_TEST_SUITE_END()
 235  } // end namespace wallet
 236