/ src / wallet / test / group_outputs_tests.cpp
group_outputs_tests.cpp
  1  // Copyright (c) 2022 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      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