Skip to content

Commit 5f7575e

Browse files
committed
Merge #12257: [wallet] Use destination groups instead of coins in coin select
232f96f doc: Add release notes for -avoidpartialspends (Karl-Johan Alm) e00b469 clean-up: Remove no longer used ivars from CInputCoin (Karl-Johan Alm) 43e04d1 wallet: Remove deprecated OutputEligibleForSpending (Karl-Johan Alm) 0128121 test: Add basic testing for wallet groups (Karl-Johan Alm) 59d6f7b wallet: Switch to using output groups instead of coins in coin selection (Karl-Johan Alm) 87ebce2 wallet: Add output grouping (Karl-Johan Alm) bb629cb Add -avoidpartialspends and m_avoid_partial_spends (Karl-Johan Alm) 65b3eda wallet: Add input bytes to CInputCoin (Karl-Johan Alm) a443d7a moveonly: CoinElegibilityFilter into coinselection.h (Karl-Johan Alm) 173e18a utils: Add insert() convenience templates (Karl-Johan Alm) Pull request description: This PR adds an optional (off by default) `-avoidpartialspends` flag, which changes coin select to use output groups rather than outputs, where each output group corresponds to all outputs with the same destination. It is a privacy improvement, as each time you spend some output, any other output that is publicly associated with the destination (address) will also be spent at the same time, at the cost of fee increase for cases where coin select without group restriction would find a more optimal set of coins (see example below). For regular use without address reuse, this PR should have no effect on the user experience whatsoever; it only affects users who, for some reason, have multiple outputs with the same destination (i.e. address reuse). Nodes with this turned off will still try to avoid partial spending, if the fee of the resulting transaction is not greater than the fee of the original transaction. Example: a node has four outputs linked to two addresses `A` and `B`: * 1.0 btc to `A` * 0.5 btc to `A` * 1.0 btc to `B` * 0.5 btc to `B` The node sends 0.2 btc to `C`. Without `-avoidpartialspends`, the following coin selection will occur: * 0.5 btc to `A` or `B` is picked * 0.2 btc is output to `C` * 0.3 - fee is output to (unique change address) With `-avoidpartialspends`, the following will instead happen: * Both of (0.5, 1.0) btc to `A` or `B` is picked (one or the other pair) * 0.2 btc is output to `C` * 1.3 - fee is output to (unique change address) As noted, the pro here is that, assuming nobody sends to the address after you spend from it, you will only ever use one address once. The con is that the transaction becomes slightly larger in this case, because it is overpicking outputs to adhere to the no partial spending rule. This complements #10386, in particular it addresses @luke-jr and @gmaxwell's concerns in bitcoin/bitcoin#10386 (comment) and bitcoin/bitcoin#10386 (comment). Together with `-avoidreuse`, this fully addresses the concerns in #10065 I believe. Tree-SHA512: 24687a4490ba59cf4198ed90052944ff4996653a4257833bb52ed24d058b3e924800c9b3790aeb6be6385b653b49e304453e5d7ff960e64c682fc23bfc447621
2 parents 7ebd8c6 + 232f96f commit 5f7575e

14 files changed

+414
-209
lines changed

doc/release-notes-pr12257.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Notable changes
2+
===============
3+
4+
Coin selection
5+
--------------
6+
- A new `-avoidpartialspends` flag has been added (default=false). If enabled, the wallet will try to spend UTXO's that point at the same destination
7+
together. This is a privacy increase, as there will no longer be cases where a wallet will inadvertently spend only parts of the coins sent to
8+
the same address (note that if someone were to send coins to that address after it was used, those coins will still be included in future
9+
coin selections).

src/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ libbitcoin_wallet_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES)
274274
libbitcoin_wallet_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
275275
libbitcoin_wallet_a_SOURCES = \
276276
interfaces/wallet.cpp \
277+
wallet/coincontrol.cpp \
277278
wallet/crypter.cpp \
278279
wallet/db.cpp \
279280
wallet/feebumper.cpp \

src/bench/coin_selection.cpp

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
#include <set>
1010

11-
static void addCoin(const CAmount& nValue, const CWallet& wallet, std::vector<COutput>& vCoins)
11+
static void addCoin(const CAmount& nValue, const CWallet& wallet, std::vector<OutputGroup>& groups)
1212
{
1313
int nInput = 0;
1414

@@ -21,7 +21,7 @@ static void addCoin(const CAmount& nValue, const CWallet& wallet, std::vector<CO
2121

2222
int nAge = 6 * 24;
2323
COutput output(wtx, nInput, nAge, true /* spendable */, true /* solvable */, true /* safe */);
24-
vCoins.push_back(output);
24+
groups.emplace_back(output.GetInputCoin(), 0, false, 0, 0);
2525
}
2626

2727
// Simple benchmark for wallet coin selection. Note that it maybe be necessary
@@ -37,37 +37,41 @@ static void CoinSelection(benchmark::State& state)
3737
LOCK(wallet.cs_wallet);
3838

3939
// Add coins.
40-
std::vector<COutput> vCoins;
40+
std::vector<OutputGroup> groups;
4141
for (int i = 0; i < 1000; ++i) {
42-
addCoin(1000 * COIN, wallet, vCoins);
42+
addCoin(1000 * COIN, wallet, groups);
4343
}
44-
addCoin(3 * COIN, wallet, vCoins);
44+
addCoin(3 * COIN, wallet, groups);
4545

4646
const CoinEligibilityFilter filter_standard(1, 6, 0);
4747
const CoinSelectionParams coin_selection_params(true, 34, 148, CFeeRate(0), 0);
4848
while (state.KeepRunning()) {
4949
std::set<CInputCoin> setCoinsRet;
5050
CAmount nValueRet;
5151
bool bnb_used;
52-
bool success = wallet.SelectCoinsMinConf(1003 * COIN, filter_standard, vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used);
52+
bool success = wallet.SelectCoinsMinConf(1003 * COIN, filter_standard, groups, setCoinsRet, nValueRet, coin_selection_params, bnb_used);
5353
assert(success);
5454
assert(nValueRet == 1003 * COIN);
5555
assert(setCoinsRet.size() == 2);
5656
}
5757
}
5858

5959
typedef std::set<CInputCoin> CoinSet;
60+
static const CWallet testWallet("dummy", WalletDatabase::CreateDummy());
61+
std::vector<std::unique_ptr<CWalletTx>> wtxn;
6062

6163
// Copied from src/wallet/test/coinselector_tests.cpp
62-
static void add_coin(const CAmount& nValue, int nInput, std::vector<CInputCoin>& set)
64+
static void add_coin(const CAmount& nValue, int nInput, std::vector<OutputGroup>& set)
6365
{
6466
CMutableTransaction tx;
6567
tx.vout.resize(nInput + 1);
6668
tx.vout[nInput].nValue = nValue;
67-
set.emplace_back(MakeTransactionRef(tx), nInput);
69+
std::unique_ptr<CWalletTx> wtx(new CWalletTx(&testWallet, MakeTransactionRef(std::move(tx))));
70+
set.emplace_back(COutput(wtx.get(), nInput, 0, true, true, true).GetInputCoin(), 0, true, 0, 0);
71+
wtxn.emplace_back(std::move(wtx));
6872
}
6973
// Copied from src/wallet/test/coinselector_tests.cpp
70-
static CAmount make_hard_case(int utxos, std::vector<CInputCoin>& utxo_pool)
74+
static CAmount make_hard_case(int utxos, std::vector<OutputGroup>& utxo_pool)
7175
{
7276
utxo_pool.clear();
7377
CAmount target = 0;
@@ -82,7 +86,7 @@ static CAmount make_hard_case(int utxos, std::vector<CInputCoin>& utxo_pool)
8286
static void BnBExhaustion(benchmark::State& state)
8387
{
8488
// Setup
85-
std::vector<CInputCoin> utxo_pool;
89+
std::vector<OutputGroup> utxo_pool;
8690
CoinSet selection;
8791
CAmount value_ret = 0;
8892
CAmount not_input_fees = 0;

src/util.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,4 +355,18 @@ std::string CopyrightHolders(const std::string& strPrefix);
355355
*/
356356
int ScheduleBatchPriority(void);
357357

358+
namespace util {
359+
360+
//! Simplification of std insertion
361+
template <typename Tdst, typename Tsrc>
362+
inline void insert(Tdst& dst, const Tsrc& src) {
363+
dst.insert(dst.begin(), src.begin(), src.end());
364+
}
365+
template <typename TsetT, typename Tsrc>
366+
inline void insert(std::set<TsetT>& dst, const Tsrc& src) {
367+
dst.insert(src.begin(), src.end());
368+
}
369+
370+
} // namespace util
371+
358372
#endif // BITCOIN_UTIL_H

src/wallet/coincontrol.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2018 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 <wallet/coincontrol.h>
6+
7+
#include <util.h>
8+
9+
void CCoinControl::SetNull()
10+
{
11+
destChange = CNoDestination();
12+
m_change_type.reset();
13+
fAllowOtherInputs = false;
14+
fAllowWatchOnly = false;
15+
m_avoid_partial_spends = gArgs.GetBoolArg("-avoidpartialspends", DEFAULT_AVOIDPARTIALSPENDS);
16+
setSelected.clear();
17+
m_feerate.reset();
18+
fOverrideFeeRate = false;
19+
m_confirm_target.reset();
20+
m_signal_bip125_rbf.reset();
21+
m_fee_mode = FeeEstimateMode::UNSET;
22+
}
23+

src/wallet/coincontrol.h

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class CCoinControl
3232
boost::optional<unsigned int> m_confirm_target;
3333
//! Override the wallet's m_signal_rbf if set
3434
boost::optional<bool> m_signal_bip125_rbf;
35+
//! Avoid partial use of funds sent to a given address
36+
bool m_avoid_partial_spends;
3537
//! Fee estimation mode to control arguments to estimateSmartFee
3638
FeeEstimateMode m_fee_mode;
3739

@@ -40,19 +42,7 @@ class CCoinControl
4042
SetNull();
4143
}
4244

43-
void SetNull()
44-
{
45-
destChange = CNoDestination();
46-
m_change_type.reset();
47-
fAllowOtherInputs = false;
48-
fAllowWatchOnly = false;
49-
setSelected.clear();
50-
m_feerate.reset();
51-
fOverrideFeeRate = false;
52-
m_confirm_target.reset();
53-
m_signal_bip125_rbf.reset();
54-
m_fee_mode = FeeEstimateMode::UNSET;
55-
}
45+
void SetNull();
5646

5747
bool HasSelected() const
5848
{

0 commit comments

Comments
 (0)