/ src / wallet / test / group_outputs_tests.cpp
group_outputs_tests.cpp
  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