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 = █ 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 = █ 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