/ src / wallet / test / fuzz / notifications.cpp
notifications.cpp
  1  // Copyright (c) 2021-present The Bitcoin Core developers
  2  // Distributed under the MIT software license, see the accompanying
  3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
  4  
  5  #include <addresstype.h>
  6  #include <consensus/amount.h>
  7  #include <interfaces/chain.h>
  8  #include <kernel/chain.h>
  9  #include <outputtype.h>
 10  #include <policy/feerate.h>
 11  #include <policy/policy.h>
 12  #include <primitives/block.h>
 13  #include <primitives/transaction.h>
 14  #include <script/descriptor.h>
 15  #include <script/script.h>
 16  #include <script/signingprovider.h>
 17  #include <sync.h>
 18  #include <test/fuzz/FuzzedDataProvider.h>
 19  #include <test/fuzz/fuzz.h>
 20  #include <test/fuzz/util.h>
 21  #include <test/util/setup_common.h>
 22  #include <tinyformat.h>
 23  #include <uint256.h>
 24  #include <util/check.h>
 25  #include <util/result.h>
 26  #include <util/translation.h>
 27  #include <wallet/coincontrol.h>
 28  #include <wallet/context.h>
 29  #include <wallet/fees.h>
 30  #include <wallet/receive.h>
 31  #include <wallet/spend.h>
 32  #include <wallet/test/util.h>
 33  #include <wallet/wallet.h>
 34  #include <wallet/walletutil.h>
 35  
 36  #include <cstddef>
 37  #include <cstdint>
 38  #include <limits>
 39  #include <numeric>
 40  #include <set>
 41  #include <string>
 42  #include <tuple>
 43  #include <utility>
 44  #include <vector>
 45  
 46  namespace wallet {
 47  namespace {
 48  const TestingSetup* g_setup;
 49  
 50  void initialize_setup()
 51  {
 52      static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
 53      g_setup = testing_setup.get();
 54  }
 55  
 56  void ImportDescriptors(CWallet& wallet, const std::string& seed_insecure)
 57  {
 58      const std::vector<std::string> DESCS{
 59          "pkh(%s/%s/*)",
 60          "sh(wpkh(%s/%s/*))",
 61          "tr(%s/%s/*)",
 62          "wpkh(%s/%s/*)",
 63      };
 64  
 65      for (const std::string& desc_fmt : DESCS) {
 66          for (bool internal : {true, false}) {
 67              const auto descriptor{(strprintf)(desc_fmt, "[5aa9973a/66h/4h/2h]" + seed_insecure, int{internal})};
 68  
 69              FlatSigningProvider keys;
 70              std::string error;
 71              auto parsed_desc = Parse(descriptor, keys, error, /*require_checksum=*/false);
 72              assert(parsed_desc);
 73              assert(error.empty());
 74              assert(parsed_desc->IsRange());
 75              assert(parsed_desc->IsSingleType());
 76              assert(!keys.keys.empty());
 77              WalletDescriptor w_desc{std::move(parsed_desc), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/1, /*next_index=*/0};
 78              assert(!wallet.GetDescriptorScriptPubKeyMan(w_desc));
 79              LOCK(wallet.cs_wallet);
 80              auto spk_manager{wallet.AddWalletDescriptor(w_desc, keys, /*label=*/"", internal)};
 81              assert(spk_manager);
 82              wallet.AddActiveScriptPubKeyMan(spk_manager->GetID(), *Assert(w_desc.descriptor->GetOutputType()), internal);
 83          }
 84      }
 85  }
 86  
 87  /**
 88   * Wraps a descriptor wallet for fuzzing.
 89   */
 90  struct FuzzedWallet {
 91      std::shared_ptr<CWallet> wallet;
 92      FuzzedWallet(const std::string& name, const std::string& seed_insecure)
 93      {
 94          auto& chain{*Assert(g_setup->m_node.chain)};
 95          wallet = std::make_shared<CWallet>(&chain, name, CreateMockableWalletDatabase());
 96          {
 97              LOCK(wallet->cs_wallet);
 98              wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
 99              auto height{*Assert(chain.getHeight())};
100              wallet->SetLastBlockProcessed(height, chain.getBlockHash(height));
101          }
102          wallet->m_keypool_size = 1; // Avoid timeout in TopUp()
103          assert(wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
104          ImportDescriptors(*wallet, seed_insecure);
105      }
106      CTxDestination GetDestination(FuzzedDataProvider& fuzzed_data_provider)
107      {
108          auto type{fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)};
109          util::Result<CTxDestination> op_dest{util::Error{}};
110          if (fuzzed_data_provider.ConsumeBool()) {
111              op_dest = wallet->GetNewDestination(type, "");
112          } else {
113              op_dest = wallet->GetNewChangeDestination(type);
114          }
115          return *Assert(op_dest);
116      }
117      CScript GetScriptPubKey(FuzzedDataProvider& fuzzed_data_provider) { return GetScriptForDestination(GetDestination(fuzzed_data_provider)); }
118      void FundTx(FuzzedDataProvider& fuzzed_data_provider, CMutableTransaction tx)
119      {
120          // The fee of "tx" is 0, so this is the total input and output amount
121          const CAmount total_amt{
122              std::accumulate(tx.vout.begin(), tx.vout.end(), CAmount{}, [](CAmount t, const CTxOut& out) { return t + out.nValue; })};
123          const uint32_t tx_size(GetVirtualTransactionSize(CTransaction{tx}));
124          std::set<int> subtract_fee_from_outputs;
125          if (fuzzed_data_provider.ConsumeBool()) {
126              for (size_t i{}; i < tx.vout.size(); ++i) {
127                  if (fuzzed_data_provider.ConsumeBool()) {
128                      subtract_fee_from_outputs.insert(i);
129                  }
130              }
131          }
132          std::vector<CRecipient> recipients;
133          for (size_t idx = 0; idx < tx.vout.size(); idx++) {
134              const CTxOut& tx_out = tx.vout[idx];
135              CTxDestination dest;
136              ExtractDestination(tx_out.scriptPubKey, dest);
137              CRecipient recipient = {dest, tx_out.nValue, subtract_fee_from_outputs.count(idx) == 1};
138              recipients.push_back(recipient);
139          }
140          CCoinControl coin_control;
141          coin_control.m_allow_other_inputs = fuzzed_data_provider.ConsumeBool();
142          CallOneOf(
143              fuzzed_data_provider, [&] { coin_control.destChange = GetDestination(fuzzed_data_provider); },
144              [&] { coin_control.m_change_type.emplace(fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)); },
145              [&] { /* no op (leave uninitialized) */ });
146          coin_control.fAllowWatchOnly = fuzzed_data_provider.ConsumeBool();
147          coin_control.m_include_unsafe_inputs = fuzzed_data_provider.ConsumeBool();
148          {
149              auto& r{coin_control.m_signal_bip125_rbf};
150              CallOneOf(
151                  fuzzed_data_provider, [&] { r = true; }, [&] { r = false; }, [&] { r = std::nullopt; });
152          }
153          coin_control.m_feerate = CFeeRate{
154              // A fee of this range should cover all cases
155              fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, 2 * total_amt),
156              tx_size,
157          };
158          if (fuzzed_data_provider.ConsumeBool()) {
159              *coin_control.m_feerate += GetMinimumFeeRate(*wallet, coin_control, nullptr);
160          }
161          coin_control.fOverrideFeeRate = fuzzed_data_provider.ConsumeBool();
162          // Add solving data (m_external_provider and SelectExternal)?
163  
164          int change_position{fuzzed_data_provider.ConsumeIntegralInRange<int>(-1, tx.vout.size() - 1)};
165          bilingual_str error;
166          // Clear tx.vout since it is not meant to be used now that we are passing outputs directly.
167          // This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly
168          tx.vout.clear();
169          (void)FundTransaction(*wallet, tx, recipients, change_position, /*lockUnspents=*/false, coin_control);
170      }
171  };
172  
173  FUZZ_TARGET(wallet_notifications, .init = initialize_setup)
174  {
175      FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
176      // The total amount, to be distributed to the wallets a and b in txs
177      // without fee. Thus, the balance of the wallets should always equal the
178      // total amount.
179      const auto total_amount{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY / 100000)};
180      FuzzedWallet a{
181          "fuzzed_wallet_a",
182          "tprv8ZgxMBicQKsPd1QwsGgzfu2pcPYbBosZhJknqreRHgsWx32nNEhMjGQX2cgFL8n6wz9xdDYwLcs78N4nsCo32cxEX8RBtwGsEGgybLiQJfk",
183      };
184      FuzzedWallet b{
185          "fuzzed_wallet_b",
186          "tprv8ZgxMBicQKsPfCunYTF18sEmEyjz8TfhGnZ3BoVAhkqLv7PLkQgmoG2Ecsp4JuqciWnkopuEwShit7st743fdmB9cMD4tznUkcs33vK51K9",
187      };
188  
189      // Keep track of all coins in this test.
190      // Each tuple in the chain represents the coins and the block created with
191      // those coins. Once the block is mined, the next tuple will have an empty
192      // block and the freshly mined coins.
193      using Coins = std::set<std::tuple<CAmount, COutPoint>>;
194      std::vector<std::tuple<Coins, CBlock>> chain;
195      {
196          // Add the initial entry
197          chain.emplace_back();
198          auto& [coins, block]{chain.back()};
199          coins.emplace(total_amount, COutPoint{Txid::FromUint256(uint256::ONE), 1});
200      }
201      LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 200)
202      {
203          CallOneOf(
204              fuzzed_data_provider,
205              [&] {
206                  auto& [coins_orig, block]{chain.back()};
207                  // Copy the coins for this block and consume all of them
208                  Coins coins = coins_orig;
209                  while (!coins.empty()) {
210                      // Create a new tx
211                      CMutableTransaction tx{};
212                      // Add some coins as inputs to it
213                      auto num_inputs{fuzzed_data_provider.ConsumeIntegralInRange<int>(1, coins.size())};
214                      CAmount in{0};
215                      while (num_inputs-- > 0) {
216                          const auto& [coin_amt, coin_outpoint]{*coins.begin()};
217                          in += coin_amt;
218                          tx.vin.emplace_back(coin_outpoint);
219                          coins.erase(coins.begin());
220                      }
221                      // Create some outputs spending all inputs, without fee
222                      LIMITED_WHILE(in > 0 && fuzzed_data_provider.ConsumeBool(), 10)
223                      {
224                          const auto out_value{ConsumeMoney(fuzzed_data_provider, in)};
225                          in -= out_value;
226                          auto& wallet{fuzzed_data_provider.ConsumeBool() ? a : b};
227                          tx.vout.emplace_back(out_value, wallet.GetScriptPubKey(fuzzed_data_provider));
228                      }
229                      // Spend the remaining input value, if any
230                      auto& wallet{fuzzed_data_provider.ConsumeBool() ? a : b};
231                      tx.vout.emplace_back(in, wallet.GetScriptPubKey(fuzzed_data_provider));
232                      // Add tx to block
233                      block.vtx.emplace_back(MakeTransactionRef(tx));
234                      // Check that funding the tx doesn't crash the wallet
235                      a.FundTx(fuzzed_data_provider, tx);
236                      b.FundTx(fuzzed_data_provider, tx);
237                  }
238                  // Mine block
239                  const uint256& hash = block.GetHash();
240                  interfaces::BlockInfo info{hash};
241                  info.prev_hash = &block.hashPrevBlock;
242                  info.height = chain.size();
243                  info.data = &block;
244                  // Ensure that no blocks are skipped by the wallet by setting the chain's accumulated
245                  // time to the maximum value. This ensures that the wallet's birth time is always
246                  // earlier than this maximum time.
247                  info.chain_time_max = std::numeric_limits<unsigned int>::max();
248                  a.wallet->blockConnected(ChainstateRole::NORMAL, info);
249                  b.wallet->blockConnected(ChainstateRole::NORMAL, info);
250                  // Store the coins for the next block
251                  Coins coins_new;
252                  for (const auto& tx : block.vtx) {
253                      uint32_t i{0};
254                      for (const auto& out : tx->vout) {
255                          coins_new.emplace(out.nValue, COutPoint{tx->GetHash(), i++});
256                      }
257                  }
258                  chain.emplace_back(coins_new, CBlock{});
259              },
260              [&] {
261                  if (chain.size() <= 1) return; // The first entry can't be removed
262                  auto& [coins, block]{chain.back()};
263                  if (block.vtx.empty()) return; // Can only disconnect if the block was submitted first
264                  // Disconnect block
265                  const uint256& hash = block.GetHash();
266                  interfaces::BlockInfo info{hash};
267                  info.prev_hash = &block.hashPrevBlock;
268                  info.height = chain.size() - 1;
269                  info.data = &block;
270                  a.wallet->blockDisconnected(info);
271                  b.wallet->blockDisconnected(info);
272                  chain.pop_back();
273              });
274          auto& [coins, first_block]{chain.front()};
275          if (!first_block.vtx.empty()) {
276              // Only check balance when at least one block was submitted
277              const auto bal_a{GetBalance(*a.wallet).m_mine_trusted};
278              const auto bal_b{GetBalance(*b.wallet).m_mine_trusted};
279              assert(total_amount == bal_a + bal_b);
280          }
281      }
282  }
283  } // namespace
284  } // namespace wallet