diff --git a/src/test/fuzz/util/wallet.h b/src/test/fuzz/util/wallet.h new file mode 100644 index 00000000000..8b55b7a985a --- /dev/null +++ b/src/test/fuzz/util/wallet.h @@ -0,0 +1,134 @@ +// Copyright (c) 2024-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_TEST_FUZZ_UTIL_WALLET_H +#define BITCOIN_TEST_FUZZ_UTIL_WALLET_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wallet { + +/** + * Wraps a descriptor wallet for fuzzing. + */ +struct FuzzedWallet { + std::shared_ptr wallet; + FuzzedWallet(interfaces::Chain& chain, const std::string& name, const std::string& seed_insecure) + { + wallet = std::make_shared(&chain, name, CreateMockableWalletDatabase()); + { + LOCK(wallet->cs_wallet); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + auto height{*Assert(chain.getHeight())}; + wallet->SetLastBlockProcessed(height, chain.getBlockHash(height)); + } + wallet->m_keypool_size = 1; // Avoid timeout in TopUp() + assert(wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)); + ImportDescriptors(seed_insecure); + } + void ImportDescriptors(const std::string& seed_insecure) + { + const std::vector DESCS{ + "pkh(%s/%s/*)", + "sh(wpkh(%s/%s/*))", + "tr(%s/%s/*)", + "wpkh(%s/%s/*)", + }; + + for (const std::string& desc_fmt : DESCS) { + for (bool internal : {true, false}) { + const auto descriptor{(strprintf)(desc_fmt, "[5aa9973a/66h/4h/2h]" + seed_insecure, int{internal})}; + + FlatSigningProvider keys; + std::string error; + auto parsed_desc = std::move(Parse(descriptor, keys, error, /*require_checksum=*/false).at(0)); + assert(parsed_desc); + assert(error.empty()); + assert(parsed_desc->IsRange()); + assert(parsed_desc->IsSingleType()); + assert(!keys.keys.empty()); + WalletDescriptor w_desc{std::move(parsed_desc), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/1, /*next_index=*/0}; + assert(!wallet->GetDescriptorScriptPubKeyMan(w_desc)); + LOCK(wallet->cs_wallet); + auto spk_manager{wallet->AddWalletDescriptor(w_desc, keys, /*label=*/"", internal)}; + assert(spk_manager); + wallet->AddActiveScriptPubKeyMan(spk_manager->GetID(), *Assert(w_desc.descriptor->GetOutputType()), internal); + } + } + } + CTxDestination GetDestination(FuzzedDataProvider& fuzzed_data_provider) + { + auto type{fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)}; + if (fuzzed_data_provider.ConsumeBool()) { + return *Assert(wallet->GetNewDestination(type, "")); + } else { + return *Assert(wallet->GetNewChangeDestination(type)); + } + } + CScript GetScriptPubKey(FuzzedDataProvider& fuzzed_data_provider) { return GetScriptForDestination(GetDestination(fuzzed_data_provider)); } + void FundTx(FuzzedDataProvider& fuzzed_data_provider, CMutableTransaction tx) + { + // The fee of "tx" is 0, so this is the total input and output amount + const CAmount total_amt{ + std::accumulate(tx.vout.begin(), tx.vout.end(), CAmount{}, [](CAmount t, const CTxOut& out) { return t + out.nValue; })}; + const uint32_t tx_size(GetVirtualTransactionSize(CTransaction{tx})); + std::set subtract_fee_from_outputs; + if (fuzzed_data_provider.ConsumeBool()) { + for (size_t i{}; i < tx.vout.size(); ++i) { + if (fuzzed_data_provider.ConsumeBool()) { + subtract_fee_from_outputs.insert(i); + } + } + } + std::vector recipients; + for (size_t idx = 0; idx < tx.vout.size(); idx++) { + const CTxOut& tx_out = tx.vout[idx]; + CTxDestination dest; + ExtractDestination(tx_out.scriptPubKey, dest); + CRecipient recipient = {dest, tx_out.nValue, subtract_fee_from_outputs.count(idx) == 1}; + recipients.push_back(recipient); + } + CCoinControl coin_control; + coin_control.m_allow_other_inputs = fuzzed_data_provider.ConsumeBool(); + CallOneOf( + fuzzed_data_provider, [&] { coin_control.destChange = GetDestination(fuzzed_data_provider); }, + [&] { coin_control.m_change_type.emplace(fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)); }, + [&] { /* no op (leave uninitialized) */ }); + coin_control.fAllowWatchOnly = fuzzed_data_provider.ConsumeBool(); + coin_control.m_include_unsafe_inputs = fuzzed_data_provider.ConsumeBool(); + { + auto& r{coin_control.m_signal_bip125_rbf}; + CallOneOf( + fuzzed_data_provider, [&] { r = true; }, [&] { r = false; }, [&] { r = std::nullopt; }); + } + coin_control.m_feerate = CFeeRate{ + // A fee of this range should cover all cases + fuzzed_data_provider.ConsumeIntegralInRange(0, 2 * total_amt), + tx_size, + }; + if (fuzzed_data_provider.ConsumeBool()) { + *coin_control.m_feerate += GetMinimumFeeRate(*wallet, coin_control, nullptr); + } + coin_control.fOverrideFeeRate = fuzzed_data_provider.ConsumeBool(); + // Add solving data (m_external_provider and SelectExternal)? + + int change_position{fuzzed_data_provider.ConsumeIntegralInRange(-1, tx.vout.size() - 1)}; + bilingual_str error; + // Clear tx.vout since it is not meant to be used now that we are passing outputs directly. + // This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly + tx.vout.clear(); + (void)FundTransaction(*wallet, tx, recipients, change_position, /*lockUnspents=*/false, coin_control); + } +}; +} + +#endif // BITCOIN_TEST_FUZZ_UTIL_WALLET_H diff --git a/src/wallet/test/fuzz/CMakeLists.txt b/src/wallet/test/fuzz/CMakeLists.txt index c30671db489..4e663977c23 100644 --- a/src/wallet/test/fuzz/CMakeLists.txt +++ b/src/wallet/test/fuzz/CMakeLists.txt @@ -11,6 +11,7 @@ target_sources(fuzz $<$:${CMAKE_CURRENT_LIST_DIR}/notifications.cpp> parse_iso8601.cpp $<$:${CMAKE_CURRENT_LIST_DIR}/scriptpubkeyman.cpp> + spend.cpp wallet_bdb_parser.cpp ) target_link_libraries(fuzz bitcoin_wallet) diff --git a/src/wallet/test/fuzz/notifications.cpp b/src/wallet/test/fuzz/notifications.cpp index a7015f6685b..1255218f3a0 100644 --- a/src/wallet/test/fuzz/notifications.cpp +++ b/src/wallet/test/fuzz/notifications.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -53,121 +54,6 @@ void initialize_setup() g_setup = testing_setup.get(); } -void ImportDescriptors(CWallet& wallet, const std::string& seed_insecure) -{ - const std::vector DESCS{ - "pkh(%s/%s/*)", - "sh(wpkh(%s/%s/*))", - "tr(%s/%s/*)", - "wpkh(%s/%s/*)", - }; - - for (const std::string& desc_fmt : DESCS) { - for (bool internal : {true, false}) { - const auto descriptor{(strprintf)(desc_fmt, "[5aa9973a/66h/4h/2h]" + seed_insecure, int{internal})}; - - FlatSigningProvider keys; - std::string error; - auto parsed_desc = std::move(Parse(descriptor, keys, error, /*require_checksum=*/false).at(0)); - assert(parsed_desc); - assert(error.empty()); - assert(parsed_desc->IsRange()); - assert(parsed_desc->IsSingleType()); - assert(!keys.keys.empty()); - WalletDescriptor w_desc{std::move(parsed_desc), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/1, /*next_index=*/0}; - assert(!wallet.GetDescriptorScriptPubKeyMan(w_desc)); - LOCK(wallet.cs_wallet); - auto spk_manager{wallet.AddWalletDescriptor(w_desc, keys, /*label=*/"", internal)}; - assert(spk_manager); - wallet.AddActiveScriptPubKeyMan(spk_manager->GetID(), *Assert(w_desc.descriptor->GetOutputType()), internal); - } - } -} - -/** - * Wraps a descriptor wallet for fuzzing. - */ -struct FuzzedWallet { - std::shared_ptr wallet; - FuzzedWallet(const std::string& name, const std::string& seed_insecure) - { - auto& chain{*Assert(g_setup->m_node.chain)}; - wallet = std::make_shared(&chain, name, CreateMockableWalletDatabase()); - { - LOCK(wallet->cs_wallet); - wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); - auto height{*Assert(chain.getHeight())}; - wallet->SetLastBlockProcessed(height, chain.getBlockHash(height)); - } - wallet->m_keypool_size = 1; // Avoid timeout in TopUp() - assert(wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)); - ImportDescriptors(*wallet, seed_insecure); - } - CTxDestination GetDestination(FuzzedDataProvider& fuzzed_data_provider) - { - auto type{fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)}; - if (fuzzed_data_provider.ConsumeBool()) { - return *Assert(wallet->GetNewDestination(type, "")); - } else { - return *Assert(wallet->GetNewChangeDestination(type)); - } - } - CScript GetScriptPubKey(FuzzedDataProvider& fuzzed_data_provider) { return GetScriptForDestination(GetDestination(fuzzed_data_provider)); } - void FundTx(FuzzedDataProvider& fuzzed_data_provider, CMutableTransaction tx) - { - // The fee of "tx" is 0, so this is the total input and output amount - const CAmount total_amt{ - std::accumulate(tx.vout.begin(), tx.vout.end(), CAmount{}, [](CAmount t, const CTxOut& out) { return t + out.nValue; })}; - const uint32_t tx_size(GetVirtualTransactionSize(CTransaction{tx})); - std::set subtract_fee_from_outputs; - if (fuzzed_data_provider.ConsumeBool()) { - for (size_t i{}; i < tx.vout.size(); ++i) { - if (fuzzed_data_provider.ConsumeBool()) { - subtract_fee_from_outputs.insert(i); - } - } - } - std::vector recipients; - for (size_t idx = 0; idx < tx.vout.size(); idx++) { - const CTxOut& tx_out = tx.vout[idx]; - CTxDestination dest; - ExtractDestination(tx_out.scriptPubKey, dest); - CRecipient recipient = {dest, tx_out.nValue, subtract_fee_from_outputs.count(idx) == 1}; - recipients.push_back(recipient); - } - CCoinControl coin_control; - coin_control.m_allow_other_inputs = fuzzed_data_provider.ConsumeBool(); - CallOneOf( - fuzzed_data_provider, [&] { coin_control.destChange = GetDestination(fuzzed_data_provider); }, - [&] { coin_control.m_change_type.emplace(fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)); }, - [&] { /* no op (leave uninitialized) */ }); - coin_control.fAllowWatchOnly = fuzzed_data_provider.ConsumeBool(); - coin_control.m_include_unsafe_inputs = fuzzed_data_provider.ConsumeBool(); - { - auto& r{coin_control.m_signal_bip125_rbf}; - CallOneOf( - fuzzed_data_provider, [&] { r = true; }, [&] { r = false; }, [&] { r = std::nullopt; }); - } - coin_control.m_feerate = CFeeRate{ - // A fee of this range should cover all cases - fuzzed_data_provider.ConsumeIntegralInRange(0, 2 * total_amt), - tx_size, - }; - if (fuzzed_data_provider.ConsumeBool()) { - *coin_control.m_feerate += GetMinimumFeeRate(*wallet, coin_control, nullptr); - } - coin_control.fOverrideFeeRate = fuzzed_data_provider.ConsumeBool(); - // Add solving data (m_external_provider and SelectExternal)? - - int change_position{fuzzed_data_provider.ConsumeIntegralInRange(-1, tx.vout.size() - 1)}; - bilingual_str error; - // Clear tx.vout since it is not meant to be used now that we are passing outputs directly. - // This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly - tx.vout.clear(); - (void)FundTransaction(*wallet, tx, recipients, change_position, /*lockUnspents=*/false, coin_control); - } -}; - FUZZ_TARGET(wallet_notifications, .init = initialize_setup) { FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; @@ -176,10 +62,12 @@ FUZZ_TARGET(wallet_notifications, .init = initialize_setup) // total amount. const auto total_amount{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY / 100000)}; FuzzedWallet a{ + *g_setup->m_node.chain, "fuzzed_wallet_a", "tprv8ZgxMBicQKsPd1QwsGgzfu2pcPYbBosZhJknqreRHgsWx32nNEhMjGQX2cgFL8n6wz9xdDYwLcs78N4nsCo32cxEX8RBtwGsEGgybLiQJfk", }; FuzzedWallet b{ + *g_setup->m_node.chain, "fuzzed_wallet_b", "tprv8ZgxMBicQKsPfCunYTF18sEmEyjz8TfhGnZ3BoVAhkqLv7PLkQgmoG2Ecsp4JuqciWnkopuEwShit7st743fdmB9cMD4tznUkcs33vK51K9", }; diff --git a/src/wallet/test/fuzz/spend.cpp b/src/wallet/test/fuzz/spend.cpp new file mode 100644 index 00000000000..9abd9e402a5 --- /dev/null +++ b/src/wallet/test/fuzz/spend.cpp @@ -0,0 +1,102 @@ +// Copyright (c) 2024-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using util::ToString; + +namespace wallet { +namespace { +const TestingSetup* g_setup; + +void initialize_setup() +{ + static const auto testing_setup = MakeNoLogFileContext(); + g_setup = testing_setup.get(); +} + +FUZZ_TARGET(wallet_create_transaction, .init = initialize_setup) +{ + SeedRandomStateForTest(SeedRand::ZEROS); + FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; + const auto& node = g_setup->m_node; + Chainstate& chainstate{node.chainman->ActiveChainstate()}; + ArgsManager& args = *node.args; + args.ForceSetArg("-dustrelayfee", ToString(fuzzed_data_provider.ConsumeIntegralInRange(0, MAX_MONEY))); + FuzzedWallet fuzzed_wallet{ + *g_setup->m_node.chain, + "fuzzed_wallet_a", + "tprv8ZgxMBicQKsPd1QwsGgzfu2pcPYbBosZhJknqreRHgsWx32nNEhMjGQX2cgFL8n6wz9xdDYwLcs78N4nsCo32cxEX8RBtwGsEGgybLiQJfk", + }; + + CCoinControl coin_control; + if (fuzzed_data_provider.ConsumeBool()) coin_control.m_version = fuzzed_data_provider.ConsumeIntegral(); + coin_control.m_avoid_partial_spends = fuzzed_data_provider.ConsumeBool(); + coin_control.m_include_unsafe_inputs = fuzzed_data_provider.ConsumeBool(); + if (fuzzed_data_provider.ConsumeBool()) coin_control.m_confirm_target = fuzzed_data_provider.ConsumeIntegral(); + coin_control.destChange = fuzzed_data_provider.ConsumeBool() ? fuzzed_wallet.GetDestination(fuzzed_data_provider) : ConsumeTxDestination(fuzzed_data_provider); + if (fuzzed_data_provider.ConsumeBool()) coin_control.m_change_type = fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES); + if (fuzzed_data_provider.ConsumeBool()) coin_control.m_feerate = CFeeRate(ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)); + coin_control.m_allow_other_inputs = fuzzed_data_provider.ConsumeBool(); + coin_control.m_locktime = fuzzed_data_provider.ConsumeIntegral(); + coin_control.fOverrideFeeRate = fuzzed_data_provider.ConsumeBool(); + + int next_locktime{0}; + CAmount all_values{0}; + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) + { + CMutableTransaction tx; + tx.nLockTime = next_locktime++; + tx.vout.resize(1); + CAmount n_value{ConsumeMoney(fuzzed_data_provider)}; + all_values += n_value; + if (all_values > MAX_MONEY) return; + tx.vout[0].nValue = n_value; + tx.vout[0].scriptPubKey = GetScriptForDestination(fuzzed_wallet.GetDestination(fuzzed_data_provider)); + LOCK(fuzzed_wallet.wallet->cs_wallet); + auto txid{tx.GetHash()}; + auto ret{fuzzed_wallet.wallet->mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateConfirmed{chainstate.m_chain.Tip()->GetBlockHash(), chainstate.m_chain.Height(), /*index=*/0}))}; + assert(ret.second); + } + + std::vector recipients; + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100) { + CTxDestination destination; + CallOneOf( + fuzzed_data_provider, + [&] { + destination = fuzzed_wallet.GetDestination(fuzzed_data_provider); + }, + [&] { + CScript script; + script << OP_RETURN; + destination = CNoDestination{script}; + }, + [&] { + destination = ConsumeTxDestination(fuzzed_data_provider); + } + ); + recipients.push_back({destination, + /*nAmount=*/ConsumeMoney(fuzzed_data_provider), + /*fSubtractFeeFromAmount=*/fuzzed_data_provider.ConsumeBool()}); + } + + std::optional change_pos; + if (fuzzed_data_provider.ConsumeBool()) change_pos = fuzzed_data_provider.ConsumeIntegral(); + (void)CreateTransaction(*fuzzed_wallet.wallet, recipients, change_pos, coin_control); +} +} // namespace +} // namespace wallet