group_outputs_tests.cpp raw

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