Skip to content

Commit 8c12fe8

Browse files
committed
Merge bitcoin#29936: fuzz: wallet: add target for CreateTransaction
c495731 fuzz: wallet: add target for `CreateTransaction` (brunoerg) 3db68e2 wallet: move `ImportDescriptors`/`FuzzedWallet` to util (brunoerg) Pull request description: This PR adds a fuzz target for the `CreateTransaction` function. It is a regression target for bitcoin#27271 and can be testing by applying: ```diff @@ -1110,7 +1110,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal( // This can only happen if feerate is 0, and requested destinations are value of 0 (e.g. OP_RETURN) // and no pre-selected inputs. This will result in 0-input transaction, which is consensus-invalid anyways if (selection_target == 0 && !coin_control.HasSelected()) { - return util::Error{_("Transaction requires one destination of non-0 value, a non-0 feerate, or a pre-selected input")}; + // return util::Error{_("Transaction requires one destination of non-0 value, a non-0 feerate, or a pre-selected input")}; } ``` Also, it moves `ImportDescriptors` function to `src/wallet/test/util.h` to avoid to duplicate same code. ACKs for top commit: marcofleon: ACK c495731 maflcko: ACK c495731 🏻 Tree-SHA512: a439f947b91b01e327e18cd18e63d5ce49f2cb9ca16ca9d56fe337b8cff239b3af4db18fe89478fe5faa5549d37ca935bd321913db7646fbf6818f825cb5d878
2 parents 947f292 + c495731 commit 8c12fe8

File tree

4 files changed

+240
-115
lines changed

4 files changed

+240
-115
lines changed

src/test/fuzz/util/wallet.h

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) 2024-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+
#ifndef BITCOIN_TEST_FUZZ_UTIL_WALLET_H
6+
#define BITCOIN_TEST_FUZZ_UTIL_WALLET_H
7+
8+
#include <test/fuzz/FuzzedDataProvider.h>
9+
#include <test/fuzz/fuzz.h>
10+
#include <test/fuzz/util.h>
11+
#include <policy/policy.h>
12+
#include <wallet/coincontrol.h>
13+
#include <wallet/fees.h>
14+
#include <wallet/spend.h>
15+
#include <wallet/test/util.h>
16+
#include <wallet/wallet.h>
17+
18+
namespace wallet {
19+
20+
/**
21+
* Wraps a descriptor wallet for fuzzing.
22+
*/
23+
struct FuzzedWallet {
24+
std::shared_ptr<CWallet> wallet;
25+
FuzzedWallet(interfaces::Chain& chain, const std::string& name, const std::string& seed_insecure)
26+
{
27+
wallet = std::make_shared<CWallet>(&chain, name, CreateMockableWalletDatabase());
28+
{
29+
LOCK(wallet->cs_wallet);
30+
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
31+
auto height{*Assert(chain.getHeight())};
32+
wallet->SetLastBlockProcessed(height, chain.getBlockHash(height));
33+
}
34+
wallet->m_keypool_size = 1; // Avoid timeout in TopUp()
35+
assert(wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
36+
ImportDescriptors(seed_insecure);
37+
}
38+
void ImportDescriptors(const std::string& seed_insecure)
39+
{
40+
const std::vector<std::string> DESCS{
41+
"pkh(%s/%s/*)",
42+
"sh(wpkh(%s/%s/*))",
43+
"tr(%s/%s/*)",
44+
"wpkh(%s/%s/*)",
45+
};
46+
47+
for (const std::string& desc_fmt : DESCS) {
48+
for (bool internal : {true, false}) {
49+
const auto descriptor{(strprintf)(desc_fmt, "[5aa9973a/66h/4h/2h]" + seed_insecure, int{internal})};
50+
51+
FlatSigningProvider keys;
52+
std::string error;
53+
auto parsed_desc = std::move(Parse(descriptor, keys, error, /*require_checksum=*/false).at(0));
54+
assert(parsed_desc);
55+
assert(error.empty());
56+
assert(parsed_desc->IsRange());
57+
assert(parsed_desc->IsSingleType());
58+
assert(!keys.keys.empty());
59+
WalletDescriptor w_desc{std::move(parsed_desc), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/1, /*next_index=*/0};
60+
assert(!wallet->GetDescriptorScriptPubKeyMan(w_desc));
61+
LOCK(wallet->cs_wallet);
62+
auto spk_manager{wallet->AddWalletDescriptor(w_desc, keys, /*label=*/"", internal)};
63+
assert(spk_manager);
64+
wallet->AddActiveScriptPubKeyMan(spk_manager->GetID(), *Assert(w_desc.descriptor->GetOutputType()), internal);
65+
}
66+
}
67+
}
68+
CTxDestination GetDestination(FuzzedDataProvider& fuzzed_data_provider)
69+
{
70+
auto type{fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)};
71+
if (fuzzed_data_provider.ConsumeBool()) {
72+
return *Assert(wallet->GetNewDestination(type, ""));
73+
} else {
74+
return *Assert(wallet->GetNewChangeDestination(type));
75+
}
76+
}
77+
CScript GetScriptPubKey(FuzzedDataProvider& fuzzed_data_provider) { return GetScriptForDestination(GetDestination(fuzzed_data_provider)); }
78+
void FundTx(FuzzedDataProvider& fuzzed_data_provider, CMutableTransaction tx)
79+
{
80+
// The fee of "tx" is 0, so this is the total input and output amount
81+
const CAmount total_amt{
82+
std::accumulate(tx.vout.begin(), tx.vout.end(), CAmount{}, [](CAmount t, const CTxOut& out) { return t + out.nValue; })};
83+
const uint32_t tx_size(GetVirtualTransactionSize(CTransaction{tx}));
84+
std::set<int> subtract_fee_from_outputs;
85+
if (fuzzed_data_provider.ConsumeBool()) {
86+
for (size_t i{}; i < tx.vout.size(); ++i) {
87+
if (fuzzed_data_provider.ConsumeBool()) {
88+
subtract_fee_from_outputs.insert(i);
89+
}
90+
}
91+
}
92+
std::vector<CRecipient> recipients;
93+
for (size_t idx = 0; idx < tx.vout.size(); idx++) {
94+
const CTxOut& tx_out = tx.vout[idx];
95+
CTxDestination dest;
96+
ExtractDestination(tx_out.scriptPubKey, dest);
97+
CRecipient recipient = {dest, tx_out.nValue, subtract_fee_from_outputs.count(idx) == 1};
98+
recipients.push_back(recipient);
99+
}
100+
CCoinControl coin_control;
101+
coin_control.m_allow_other_inputs = fuzzed_data_provider.ConsumeBool();
102+
CallOneOf(
103+
fuzzed_data_provider, [&] { coin_control.destChange = GetDestination(fuzzed_data_provider); },
104+
[&] { coin_control.m_change_type.emplace(fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)); },
105+
[&] { /* no op (leave uninitialized) */ });
106+
coin_control.fAllowWatchOnly = fuzzed_data_provider.ConsumeBool();
107+
coin_control.m_include_unsafe_inputs = fuzzed_data_provider.ConsumeBool();
108+
{
109+
auto& r{coin_control.m_signal_bip125_rbf};
110+
CallOneOf(
111+
fuzzed_data_provider, [&] { r = true; }, [&] { r = false; }, [&] { r = std::nullopt; });
112+
}
113+
coin_control.m_feerate = CFeeRate{
114+
// A fee of this range should cover all cases
115+
fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, 2 * total_amt),
116+
tx_size,
117+
};
118+
if (fuzzed_data_provider.ConsumeBool()) {
119+
*coin_control.m_feerate += GetMinimumFeeRate(*wallet, coin_control, nullptr);
120+
}
121+
coin_control.fOverrideFeeRate = fuzzed_data_provider.ConsumeBool();
122+
// Add solving data (m_external_provider and SelectExternal)?
123+
124+
int change_position{fuzzed_data_provider.ConsumeIntegralInRange<int>(-1, tx.vout.size() - 1)};
125+
bilingual_str error;
126+
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly.
127+
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly
128+
tx.vout.clear();
129+
(void)FundTransaction(*wallet, tx, recipients, change_position, /*lockUnspents=*/false, coin_control);
130+
}
131+
};
132+
}
133+
134+
#endif // BITCOIN_TEST_FUZZ_UTIL_WALLET_H

src/wallet/test/fuzz/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ target_sources(fuzz
1111
$<$<BOOL:${USE_SQLITE}>:${CMAKE_CURRENT_LIST_DIR}/notifications.cpp>
1212
parse_iso8601.cpp
1313
$<$<BOOL:${USE_SQLITE}>:${CMAKE_CURRENT_LIST_DIR}/scriptpubkeyman.cpp>
14+
spend.cpp
1415
wallet_bdb_parser.cpp
1516
)
1617
target_link_libraries(fuzz bitcoin_wallet)

src/wallet/test/fuzz/notifications.cpp

Lines changed: 3 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <test/fuzz/FuzzedDataProvider.h>
1919
#include <test/fuzz/fuzz.h>
2020
#include <test/fuzz/util.h>
21+
#include <test/fuzz/util/wallet.h>
2122
#include <test/util/setup_common.h>
2223
#include <tinyformat.h>
2324
#include <uint256.h>
@@ -53,121 +54,6 @@ void initialize_setup()
5354
g_setup = testing_setup.get();
5455
}
5556

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 = std::move(Parse(descriptor, keys, error, /*require_checksum=*/false).at(0));
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-
if (fuzzed_data_provider.ConsumeBool()) {
110-
return *Assert(wallet->GetNewDestination(type, ""));
111-
} else {
112-
return *Assert(wallet->GetNewChangeDestination(type));
113-
}
114-
}
115-
CScript GetScriptPubKey(FuzzedDataProvider& fuzzed_data_provider) { return GetScriptForDestination(GetDestination(fuzzed_data_provider)); }
116-
void FundTx(FuzzedDataProvider& fuzzed_data_provider, CMutableTransaction tx)
117-
{
118-
// The fee of "tx" is 0, so this is the total input and output amount
119-
const CAmount total_amt{
120-
std::accumulate(tx.vout.begin(), tx.vout.end(), CAmount{}, [](CAmount t, const CTxOut& out) { return t + out.nValue; })};
121-
const uint32_t tx_size(GetVirtualTransactionSize(CTransaction{tx}));
122-
std::set<int> subtract_fee_from_outputs;
123-
if (fuzzed_data_provider.ConsumeBool()) {
124-
for (size_t i{}; i < tx.vout.size(); ++i) {
125-
if (fuzzed_data_provider.ConsumeBool()) {
126-
subtract_fee_from_outputs.insert(i);
127-
}
128-
}
129-
}
130-
std::vector<CRecipient> recipients;
131-
for (size_t idx = 0; idx < tx.vout.size(); idx++) {
132-
const CTxOut& tx_out = tx.vout[idx];
133-
CTxDestination dest;
134-
ExtractDestination(tx_out.scriptPubKey, dest);
135-
CRecipient recipient = {dest, tx_out.nValue, subtract_fee_from_outputs.count(idx) == 1};
136-
recipients.push_back(recipient);
137-
}
138-
CCoinControl coin_control;
139-
coin_control.m_allow_other_inputs = fuzzed_data_provider.ConsumeBool();
140-
CallOneOf(
141-
fuzzed_data_provider, [&] { coin_control.destChange = GetDestination(fuzzed_data_provider); },
142-
[&] { coin_control.m_change_type.emplace(fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)); },
143-
[&] { /* no op (leave uninitialized) */ });
144-
coin_control.fAllowWatchOnly = fuzzed_data_provider.ConsumeBool();
145-
coin_control.m_include_unsafe_inputs = fuzzed_data_provider.ConsumeBool();
146-
{
147-
auto& r{coin_control.m_signal_bip125_rbf};
148-
CallOneOf(
149-
fuzzed_data_provider, [&] { r = true; }, [&] { r = false; }, [&] { r = std::nullopt; });
150-
}
151-
coin_control.m_feerate = CFeeRate{
152-
// A fee of this range should cover all cases
153-
fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, 2 * total_amt),
154-
tx_size,
155-
};
156-
if (fuzzed_data_provider.ConsumeBool()) {
157-
*coin_control.m_feerate += GetMinimumFeeRate(*wallet, coin_control, nullptr);
158-
}
159-
coin_control.fOverrideFeeRate = fuzzed_data_provider.ConsumeBool();
160-
// Add solving data (m_external_provider and SelectExternal)?
161-
162-
int change_position{fuzzed_data_provider.ConsumeIntegralInRange<int>(-1, tx.vout.size() - 1)};
163-
bilingual_str error;
164-
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly.
165-
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly
166-
tx.vout.clear();
167-
(void)FundTransaction(*wallet, tx, recipients, change_position, /*lockUnspents=*/false, coin_control);
168-
}
169-
};
170-
17157
FUZZ_TARGET(wallet_notifications, .init = initialize_setup)
17258
{
17359
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
@@ -176,10 +62,12 @@ FUZZ_TARGET(wallet_notifications, .init = initialize_setup)
17662
// total amount.
17763
const auto total_amount{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY / 100000)};
17864
FuzzedWallet a{
65+
*g_setup->m_node.chain,
17966
"fuzzed_wallet_a",
18067
"tprv8ZgxMBicQKsPd1QwsGgzfu2pcPYbBosZhJknqreRHgsWx32nNEhMjGQX2cgFL8n6wz9xdDYwLcs78N4nsCo32cxEX8RBtwGsEGgybLiQJfk",
18168
};
18269
FuzzedWallet b{
70+
*g_setup->m_node.chain,
18371
"fuzzed_wallet_b",
18472
"tprv8ZgxMBicQKsPfCunYTF18sEmEyjz8TfhGnZ3BoVAhkqLv7PLkQgmoG2Ecsp4JuqciWnkopuEwShit7st743fdmB9cMD4tznUkcs33vK51K9",
18573
};

src/wallet/test/fuzz/spend.cpp

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) 2024-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 <test/fuzz/FuzzedDataProvider.h>
6+
#include <test/fuzz/fuzz.h>
7+
#include <test/fuzz/util.h>
8+
#include <test/fuzz/util/wallet.h>
9+
#include <test/util/random.h>
10+
#include <test/util/setup_common.h>
11+
#include <wallet/coincontrol.h>
12+
#include <wallet/context.h>
13+
#include <wallet/spend.h>
14+
#include <wallet/test/util.h>
15+
#include <wallet/wallet.h>
16+
#include <validation.h>
17+
#include <addresstype.h>
18+
19+
using util::ToString;
20+
21+
namespace wallet {
22+
namespace {
23+
const TestingSetup* g_setup;
24+
25+
void initialize_setup()
26+
{
27+
static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
28+
g_setup = testing_setup.get();
29+
}
30+
31+
FUZZ_TARGET(wallet_create_transaction, .init = initialize_setup)
32+
{
33+
SeedRandomStateForTest(SeedRand::ZEROS);
34+
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
35+
const auto& node = g_setup->m_node;
36+
Chainstate& chainstate{node.chainman->ActiveChainstate()};
37+
ArgsManager& args = *node.args;
38+
args.ForceSetArg("-dustrelayfee", ToString(fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY)));
39+
FuzzedWallet fuzzed_wallet{
40+
*g_setup->m_node.chain,
41+
"fuzzed_wallet_a",
42+
"tprv8ZgxMBicQKsPd1QwsGgzfu2pcPYbBosZhJknqreRHgsWx32nNEhMjGQX2cgFL8n6wz9xdDYwLcs78N4nsCo32cxEX8RBtwGsEGgybLiQJfk",
43+
};
44+
45+
CCoinControl coin_control;
46+
if (fuzzed_data_provider.ConsumeBool()) coin_control.m_version = fuzzed_data_provider.ConsumeIntegral<unsigned int>();
47+
coin_control.m_avoid_partial_spends = fuzzed_data_provider.ConsumeBool();
48+
coin_control.m_include_unsafe_inputs = fuzzed_data_provider.ConsumeBool();
49+
if (fuzzed_data_provider.ConsumeBool()) coin_control.m_confirm_target = fuzzed_data_provider.ConsumeIntegral<unsigned int>();
50+
coin_control.destChange = fuzzed_data_provider.ConsumeBool() ? fuzzed_wallet.GetDestination(fuzzed_data_provider) : ConsumeTxDestination(fuzzed_data_provider);
51+
if (fuzzed_data_provider.ConsumeBool()) coin_control.m_change_type = fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES);
52+
if (fuzzed_data_provider.ConsumeBool()) coin_control.m_feerate = CFeeRate(ConsumeMoney(fuzzed_data_provider, /*max=*/COIN));
53+
coin_control.m_allow_other_inputs = fuzzed_data_provider.ConsumeBool();
54+
coin_control.m_locktime = fuzzed_data_provider.ConsumeIntegral<unsigned int>();
55+
coin_control.fOverrideFeeRate = fuzzed_data_provider.ConsumeBool();
56+
57+
int next_locktime{0};
58+
CAmount all_values{0};
59+
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000)
60+
{
61+
CMutableTransaction tx;
62+
tx.nLockTime = next_locktime++;
63+
tx.vout.resize(1);
64+
CAmount n_value{ConsumeMoney(fuzzed_data_provider)};
65+
all_values += n_value;
66+
if (all_values > MAX_MONEY) return;
67+
tx.vout[0].nValue = n_value;
68+
tx.vout[0].scriptPubKey = GetScriptForDestination(fuzzed_wallet.GetDestination(fuzzed_data_provider));
69+
LOCK(fuzzed_wallet.wallet->cs_wallet);
70+
auto txid{tx.GetHash()};
71+
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}))};
72+
assert(ret.second);
73+
}
74+
75+
std::vector<CRecipient> recipients;
76+
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100) {
77+
CTxDestination destination;
78+
CallOneOf(
79+
fuzzed_data_provider,
80+
[&] {
81+
destination = fuzzed_wallet.GetDestination(fuzzed_data_provider);
82+
},
83+
[&] {
84+
CScript script;
85+
script << OP_RETURN;
86+
destination = CNoDestination{script};
87+
},
88+
[&] {
89+
destination = ConsumeTxDestination(fuzzed_data_provider);
90+
}
91+
);
92+
recipients.push_back({destination,
93+
/*nAmount=*/ConsumeMoney(fuzzed_data_provider),
94+
/*fSubtractFeeFromAmount=*/fuzzed_data_provider.ConsumeBool()});
95+
}
96+
97+
std::optional<unsigned int> change_pos;
98+
if (fuzzed_data_provider.ConsumeBool()) change_pos = fuzzed_data_provider.ConsumeIntegral<unsigned int>();
99+
(void)CreateTransaction(*fuzzed_wallet.wallet, recipients, change_pos, coin_control);
100+
}
101+
} // namespace
102+
} // namespace wallet

0 commit comments

Comments
 (0)