LCOV - code coverage report
Current view: top level - src/wallet/test - group_outputs_tests.cpp (source / functions) Coverage Total Hit
Test: total_coverage.info Lines: 100.0 % 75 75
Test Date: 2025-09-13 05:20:27 Functions: 100.0 % 7 7
Branches: 52.0 % 246 128

             Branch data     Line data    Source code
       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                 :           1 : static std::shared_ptr<CWallet> NewWallet(const node::NodeContext& m_node)
      20                 :             : {
      21   [ +  -  +  - ]:           1 :     std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase());
      22         [ +  - ]:           1 :     LOCK(wallet->cs_wallet);
      23         [ +  - ]:           1 :     wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
      24         [ +  - ]:           1 :     wallet->SetupDescriptorScriptPubKeyMans();
      25   [ +  -  +  - ]:           1 :     return wallet;
      26         [ -  + ]:           1 : }
      27                 :             : 
      28                 :         124 : 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                 :         124 :     CMutableTransaction tx;
      37                 :         124 :     tx.nLockTime = nextLockTime++;        // so all transactions get different hashes
      38         [ +  - ]:         124 :     tx.vout.resize(1);
      39         [ +  - ]:         124 :     tx.vout[0].nValue = nValue;
      40         [ +  - ]:         124 :     tx.vout[0].scriptPubKey = GetScriptForDestination(dest);
      41                 :             : 
      42         [ +  - ]:         124 :     const auto txid{tx.GetHash()};
      43         [ +  - ]:         124 :     LOCK(wallet.cs_wallet);
      44   [ +  -  +  -  :         248 :     auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{}));
                   -  + ]
      45         [ -  + ]:         124 :     assert(ret.second);
      46         [ +  - ]:         124 :     CWalletTx& wtx = (*ret.first).second;
      47         [ +  - ]:         124 :     const auto& txout = wtx.tx->vout.at(0);
      48   [ +  -  +  -  :         248 :     coins.Add(*Assert(OutputTypeFromDestination(dest)),
          +  -  +  -  +  
             -  +  -  +  
                      - ]
      49         [ +  - ]:         124 :               {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         [ +  - ]:         124 :                    fee_rate});
      58                 :         248 : }
      59                 :             : 
      60                 :          16 :  CoinSelectionParams makeSelectionParams(FastRandomContext& rand, bool avoid_partial_spends)
      61                 :             : {
      62                 :          16 :     return CoinSelectionParams{
      63                 :             :             rand,
      64                 :             :             /*change_output_size=*/ 0,
      65                 :             :             /*change_spend_size=*/ 0,
      66                 :             :             /*min_change_target=*/ CENT,
      67                 :          16 :             /*effective_feerate=*/ CFeeRate(0),
      68                 :          16 :             /*long_term_feerate=*/ CFeeRate(0),
      69                 :          16 :             /*discard_feerate=*/ CFeeRate(0),
      70                 :             :             /*tx_noinputs_size=*/ 0,
      71                 :             :             /*avoid_partial=*/ avoid_partial_spends,
      72                 :          16 :     };
      73                 :             : }
      74                 :             : 
      75                 :             : class GroupVerifier
      76                 :             : {
      77                 :             : public:
      78                 :             :     std::shared_ptr<CWallet> wallet{nullptr};
      79                 :             :     CoinsResult coins_pool;
      80                 :             :     FastRandomContext rand;
      81                 :             : 
      82                 :          16 :     void GroupVerify(const OutputType type,
      83                 :             :                      const CoinEligibilityFilter& filter,
      84                 :             :                      bool avoid_partial_spends,
      85                 :             :                      bool positive_only,
      86                 :             :                      int expected_size)
      87                 :             :     {
      88   [ +  -  +  -  :          32 :         OutputGroupTypeMap groups = GroupOutputs(*wallet, coins_pool, makeSelectionParams(rand, avoid_partial_spends), {{filter}})[filter];
             +  -  +  + ]
      89         [ +  - ]:           6 :         std::vector<OutputGroup>& groups_out = positive_only ? groups.groups_by_type[type].positive_group :
      90   [ +  +  +  - ]:          22 :                                                groups.groups_by_type[type].mixed_group;
      91   [ +  -  -  +  :          16 :         BOOST_CHECK_EQUAL(groups_out.size(), expected_size);
                   +  - ]
      92                 :          16 :     }
      93                 :             : 
      94                 :           8 :     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                 :           8 :         GroupVerify(type, filter, /*avoid_partial_spends=*/false, positive_only,  expected_with_partial_spends_size);
     102                 :             :         // Second don't avoid partial spends
     103                 :           8 :         GroupVerify(type, filter, /*avoid_partial_spends=*/true, positive_only, expected_without_partial_spends_size);
     104                 :           8 :     }
     105                 :             : };
     106                 :             : 
     107   [ +  -  +  -  :           7 : BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
          +  -  +  -  -  
          +  +  -  +  -  
          +  -  +  -  +  
          -  +  -  -  +  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  -  +  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  -  +  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  -  +  +  
                      - ]
     108                 :             : {
     109                 :           1 :     const auto& wallet = NewWallet(m_node);
     110                 :           1 :     GroupVerifier group_verifier;
     111                 :           1 :     group_verifier.wallet = wallet;
     112                 :             : 
     113         [ +  - ]:           1 :     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                 :           1 :     unsigned long GROUP_SIZE = 10;
     122   [ +  -  +  -  :           2 :     const CTxDestination dest = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
             +  -  +  - ]
     123         [ +  + ]:          11 :     for (unsigned long i = 0; i < GROUP_SIZE; i++) {
     124         [ +  - ]:          10 :         addCoin(group_verifier.coins_pool, *wallet, dest, 10 * COIN, /*is_from_me=*/true);
     125                 :             :     }
     126                 :             : 
     127         [ +  - ]:           1 :     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   [ +  -  +  -  :           2 :     const CTxDestination dest2 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
             +  -  +  - ]
     139         [ +  + ]:          11 :     for (unsigned long i = 0; i < GROUP_SIZE; i++) {
     140         [ +  - ]:          10 :         addCoin(group_verifier.coins_pool, *wallet, dest2, 5 * COIN, /*is_from_me=*/true);
     141                 :             :     }
     142                 :             : 
     143         [ +  - ]:           1 :     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   [ +  -  +  -  :           2 :     const CTxDestination dest3 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
             +  -  +  - ]
     154         [ +  - ]:           1 :     addCoin(group_verifier.coins_pool, *wallet, dest3, 1, true, CFeeRate(100));
     155   [ +  -  +  -  :           2 :     BOOST_CHECK(group_verifier.coins_pool.coins[OutputType::BECH32].back().GetEffectiveValue() <= 0);
             +  -  +  - ]
     156                 :             : 
     157                 :             :     // First expect no changes with "positive_only" enabled
     158         [ +  - ]:           1 :     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         [ +  - ]:           1 :     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   [ +  -  +  -  :           2 :     const CTxDestination dest4 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
             +  -  +  - ]
     178         [ +  - ]:           1 :     addCoin(group_verifier.coins_pool, *wallet, dest4, 6 * COIN,
     179         [ +  - ]:           1 :             /*is_from_me=*/false, CFeeRate(0), /*depth=*/5);
     180                 :             : 
     181                 :             :     // Expect no changes from this round and the previous one (point 4)
     182         [ +  - ]:           1 :     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   [ +  -  +  -  :           2 :     const CTxDestination dest5 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
             +  -  +  - ]
     195         [ +  - ]:           1 :     addCoin(group_verifier.coins_pool, *wallet, dest5, 6 * COIN,
     196         [ +  - ]:           1 :             /*is_from_me=*/true, CFeeRate(0), /*depth=*/0);
     197                 :             : 
     198                 :             :     // Expect no changes from this round and the previous one (point 5)
     199         [ +  - ]:           1 :     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   [ +  -  +  -  :           2 :     const CTxDestination dest7 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
             +  -  +  - ]
     210                 :           1 :     uint16_t NUM_SINGLE_ENTRIES = 101;
     211         [ +  + ]:         102 :     for (unsigned long i = 0; i < NUM_SINGLE_ENTRIES; i++) { // OUTPUT_GROUP_MAX_ENTRIES{100}
     212         [ +  - ]:         101 :         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                 :           1 :     int PREVIOUS_ROUND_COUNT = GROUP_SIZE * 2 + 1;
     217         [ +  - ]:           1 :     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         [ +  - ]:           1 :     const CoinEligibilityFilter& avoid_partial_groups_filter{1, 6, 0, 0, /*include_partial=*/ true};
     225         [ +  - ]:           1 :     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         [ +  - ]:           2 : }
     231                 :             : 
     232                 :             : BOOST_AUTO_TEST_SUITE_END()
     233                 :             : } // end namespace wallet
        

Generated by: LCOV version 2.0-1