Skip to content

wallet: -avoidreuse with destination filters #13801

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/wallet/coincontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ void CCoinControl::SetNull()
fAllowOtherInputs = false;
fAllowWatchOnly = false;
m_avoid_partial_spends = gArgs.GetBoolArg("-avoidpartialspends", DEFAULT_AVOIDPARTIALSPENDS);
m_dest_filter = DeriveDestinationFilter();
setSelected.clear();
m_feerate.reset();
fOverrideFeeRate = false;
m_confirm_target.reset();
m_signal_bip125_rbf.reset();
m_fee_mode = FeeEstimateMode::UNSET;
}

2 changes: 2 additions & 0 deletions src/wallet/coincontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class CCoinControl
boost::optional<bool> m_signal_bip125_rbf;
//! Avoid partial use of funds sent to a given address
bool m_avoid_partial_spends;
//! Destination filter (allow all, or allow only clean, or allow only dirty outputs)
DestinationFilter m_dest_filter;
//! Fee estimation mode to control arguments to estimateSmartFee
FeeEstimateMode m_fee_mode;

Expand Down
23 changes: 23 additions & 0 deletions src/wallet/coinselection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,26 @@ bool OutputGroup::EligibleForSpending(const CoinEligibilityFilter& eligibility_f
&& m_ancestors <= eligibility_filter.max_ancestors
&& m_descendants <= eligibility_filter.max_descendants;
}

static const std::string DESTINATION_FILTER_STRING_MIXED = "mixed";
static const std::string DESTINATION_FILTER_STRING_CLEAN = "clean";
static const std::string DESTINATION_FILTER_STRING_DIRTY = "dirty";

bool ParseDestinationFilter(const std::string& type, DestinationFilter& dest_filter)
{
if (type == DESTINATION_FILTER_STRING_MIXED) dest_filter = DestinationFilter::DestinationMixed;
else if (type == DESTINATION_FILTER_STRING_CLEAN) dest_filter = DestinationFilter::DestinationOnlyClean;
else if (type == DESTINATION_FILTER_STRING_DIRTY) dest_filter = DestinationFilter::DestinationOnlyDirty;
else return false;
return true;
}

const std::string& FormatDestinationFilter(DestinationFilter type)
{
switch (type) {
case DestinationFilter::DestinationMixed: return DESTINATION_FILTER_STRING_MIXED;
case DestinationFilter::DestinationOnlyClean: return DESTINATION_FILTER_STRING_CLEAN;
case DestinationFilter::DestinationOnlyDirty: return DESTINATION_FILTER_STRING_DIRTY;
default: assert(false);
}
}
15 changes: 15 additions & 0 deletions src/wallet/coinselection.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ class CInputCoin {
}
};

enum DestinationFilter
{
DestinationMixed = 0,
DestinationOnlyClean = 1,
DestinationOnlyDirty = 2,
};

bool ParseDestinationFilter(const std::string& str, DestinationFilter& dest_filter);
const std::string& FormatDestinationFilter(DestinationFilter dest_filter);
inline bool DestinationFilterApplies(DestinationFilter dest_filter, bool is_dirty) {
return
(dest_filter == DestinationFilter::DestinationMixed) ||
((dest_filter == DestinationOnlyDirty) == is_dirty);
}

struct CoinEligibilityFilter
{
const int conf_mine;
Expand Down
1 change: 1 addition & 0 deletions src/wallet/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ void WalletInit::AddWalletOptions() const
{
gArgs.AddArg("-addresstype", strprintf("What type of addresses to use (\"legacy\", \"p2sh-segwit\", or \"bech32\", default: \"%s\")", FormatOutputType(DEFAULT_ADDRESS_TYPE)), false, OptionsCategory::WALLET);
gArgs.AddArg("-avoidpartialspends", strprintf(_("Group outputs by address, selecting all or none, instead of selecting on a per-output basis. Privacy is improved as an address is only used once (unless someone sends to it after spending from it), but may result in slightly higher fees as suboptimal coin selection may result due to the added limitation (default: %u)"), DEFAULT_AVOIDPARTIALSPENDS), false, OptionsCategory::WALLET);
gArgs.AddArg("-avoidreuse", "Mark addresses which have been used to fund transactions in the past, and avoid reusing these in future funding, except when explicitly requested " + strprintf(_("(default: %u)"), DEFAULT_AVOIDREUSE), false, OptionsCategory::WALLET);
gArgs.AddArg("-changetype", "What type of change to use (\"legacy\", \"p2sh-segwit\", or \"bech32\"). Default is same as -addresstype, except when -addresstype=p2sh-segwit a native segwit output is used when sending to a native segwit address)", false, OptionsCategory::WALLET);
gArgs.AddArg("-disablewallet", "Do not load the wallet and disable wallet RPC calls", false, OptionsCategory::WALLET);
gArgs.AddArg("-discardfee=<amt>", strprintf("The fee rate (in %s/kB) that indicates your tolerance for discarding change by adding it to the fee (default: %s). "
Expand Down
38 changes: 30 additions & 8 deletions src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@

static const std::string WALLET_ENDPOINT_BASE = "/wallet/";

inline DestinationFilter DestinationFilterFromValue(const UniValue& value)
{
DestinationFilter dest_filter;
if (value.isNull()) return DeriveDestinationFilter();
if (!ParseDestinationFilter(value.get_str(), dest_filter)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "dest_filter must be one of 'mixed', 'clean', or 'dirty'");
}
return dest_filter;
}

bool GetWalletNameFromJSONRPCRequest(const JSONRPCRequest& request, std::string& wallet_name)
{
if (request.URI.substr(0, WALLET_ENDPOINT_BASE.size()) == WALLET_ENDPOINT_BASE) {
Expand Down Expand Up @@ -464,7 +474,7 @@ static UniValue getaddressesbyaccount(const JSONRPCRequest& request)

static CTransactionRef SendMoney(CWallet * const pwallet, const CTxDestination &address, CAmount nValue, bool fSubtractFeeFromAmount, const CCoinControl& coin_control, mapValue_t mapValue, std::string fromAccount)
{
CAmount curBalance = pwallet->GetBalance();
CAmount curBalance = pwallet->GetBalance(ISMINE_SPENDABLE, 0, coin_control.m_dest_filter);

// Check amount
if (nValue <= 0)
Expand Down Expand Up @@ -511,9 +521,9 @@ static UniValue sendtoaddress(const JSONRPCRequest& request)
return NullUniValue;
}

if (request.fHelp || request.params.size() < 2 || request.params.size() > 8)
if (request.fHelp || request.params.size() < 2 || request.params.size() > 9)
throw std::runtime_error(
"sendtoaddress \"address\" amount ( \"comment\" \"comment_to\" subtractfeefromamount replaceable conf_target \"estimate_mode\")\n"
"sendtoaddress \"address\" amount ( \"comment\" \"comment_to\" subtractfeefromamount replaceable conf_target \"estimate_mode\" dest_filter )\n"
"\nSend an amount to a given address.\n"
+ HelpRequiringPassphrase(pwallet) +
"\nArguments:\n"
Expand All @@ -532,12 +542,17 @@ static UniValue sendtoaddress(const JSONRPCRequest& request)
" \"UNSET\"\n"
" \"ECONOMICAL\"\n"
" \"CONSERVATIVE\"\n"
"9. dest_filter (string, optional) Destination filter (only applicable if -avoidreuse is enabled), one of 'mixed', 'clean', or 'dirty'\n"
" 'mixed' will include both clean and dirty outputs (default if -avoidreuse=false)\n"
" 'clean' will include clean outputs only (default if -avoidreuse=true)\n"
" 'dirty' will include dirty outputs only\n"
"\nResult:\n"
"\"txid\" (string) The transaction id.\n"
"\nExamples:\n"
+ HelpExampleCli("sendtoaddress", "\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 0.1")
+ HelpExampleCli("sendtoaddress", "\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 0.1 \"donation\" \"seans outpost\"")
+ HelpExampleCli("sendtoaddress", "\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 0.1 \"\" \"\" true")
+ HelpExampleCli("sendtoaddress", "\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 0.1 \"\" \"\" false true 0 UNSET dirty")
+ HelpExampleRpc("sendtoaddress", "\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\", 0.1, \"donation\", \"seans outpost\"")
);

Expand Down Expand Up @@ -584,6 +599,7 @@ static UniValue sendtoaddress(const JSONRPCRequest& request)
}
}

if (!request.params[8].isNull()) coin_control.m_dest_filter = DestinationFilterFromValue(request.params[8]);

EnsureWalletIsUnlocked(pwallet);

Expand Down Expand Up @@ -861,10 +877,10 @@ static UniValue getbalance(const JSONRPCRequest& request)
return NullUniValue;
}

if (request.fHelp || (request.params.size() > 3 ))
if (request.fHelp || request.params.size() > 4)
throw std::runtime_error(
(IsDeprecatedRPCEnabled("accounts") ? std::string(
"getbalance ( \"account\" minconf include_watchonly )\n"
"getbalance ( \"account\" minconf include_watchonly dest_filter )\n"
"\nIf account is not specified, returns the server's total available balance.\n"
"The available balance is what the wallet considers currently spendable, and is\n"
"thus affected by options which limit spendability such as -spendzeroconfchange.\n"
Expand Down Expand Up @@ -896,6 +912,10 @@ static UniValue getbalance(const JSONRPCRequest& request)
"1. (dummy) (string, optional) Remains for backward compatibility. Must be excluded or set to \"*\".\n"
"2. minconf (numeric, optional, default=0) Only include transactions confirmed at least this many times.\n")) +
"3. include_watchonly (bool, optional, default=false) Also include balance in watch-only addresses (see 'importaddress')\n"
"4. dest_filter (string, optional) Destination filter (only applicable if -avoidreuse is enabled), one of 'mixed', 'clean', or 'dirty'\n"
" 'mixed' will show balance for both clean and dirty outputs (default if -avoidreuse=false)\n"
" 'clean' will show balance for clean outputs only (default if -avoidreuse=true)\n"
" 'dirty' will show balance for dirty outputs only\n"
"\nResult:\n"
"amount (numeric) The total amount in " + CURRENCY_UNIT + " received for this account.\n"
"\nExamples:\n"
Expand Down Expand Up @@ -929,6 +949,8 @@ static UniValue getbalance(const JSONRPCRequest& request)
filter = filter | ISMINE_WATCH_ONLY;
}

DestinationFilter dest_filter = DestinationFilterFromValue(request.params[3]);

if (!account_value.isNull()) {

const std::string& account_param = account_value.get_str();
Expand All @@ -941,7 +963,7 @@ static UniValue getbalance(const JSONRPCRequest& request)
}
}

return ValueFromAmount(pwallet->GetBalance(filter, min_depth));
return ValueFromAmount(pwallet->GetBalance(filter, min_depth, dest_filter));
}

static UniValue getunconfirmedbalance(const JSONRPCRequest &request)
Expand Down Expand Up @@ -4781,7 +4803,7 @@ static const CRPCCommand commands[] =
{ "wallet", "dumpwallet", &dumpwallet, {"filename"} },
{ "wallet", "encryptwallet", &encryptwallet, {"passphrase"} },
{ "wallet", "getaddressinfo", &getaddressinfo, {"address"} },
{ "wallet", "getbalance", &getbalance, {"account|dummy","minconf","include_watchonly"} },
{ "wallet", "getbalance", &getbalance, {"account|dummy","minconf","include_watchonly","dest_filter"} },
{ "wallet", "getnewaddress", &getnewaddress, {"label|account","address_type"} },
{ "wallet", "getrawchangeaddress", &getrawchangeaddress, {"address_type"} },
{ "wallet", "getreceivedbyaddress", &getreceivedbyaddress, {"address","minconf"} },
Expand All @@ -4805,7 +4827,7 @@ static const CRPCCommand commands[] =
{ "wallet", "loadwallet", &loadwallet, {"filename"} },
{ "wallet", "lockunspent", &lockunspent, {"unlock","transactions"} },
{ "wallet", "sendmany", &sendmany, {"fromaccount|dummy","amounts","minconf","comment","subtractfeefrom","replaceable","conf_target","estimate_mode"} },
{ "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","replaceable","conf_target","estimate_mode"} },
{ "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","replaceable","conf_target","estimate_mode","dest_filter"} },
{ "wallet", "settxfee", &settxfee, {"amount"} },
{ "wallet", "signmessage", &signmessage, {"address","message"} },
{ "wallet", "signrawtransactionwithwallet", &signrawtransactionwithwallet, {"hexstring","prevtxs","sighashtype"} },
Expand Down
68 changes: 56 additions & 12 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,35 @@ bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash)
return success;
}

void CWallet::SetDirtyState(const uint256& hash, unsigned int n, bool dirty)
{
const CWalletTx* srctx = GetWalletTx(hash);
if (!srctx) return;

CTxDestination dst;
if (ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst)) {
if (::IsMine(*this, dst)) {
if (dirty && !GetDestData(dst, "dirty", nullptr)) {
AddDestData(dst, "dirty", "p"); // p for "present", opposite of absent (null)
} else if (!dirty && GetDestData(dst, "dirty", nullptr)) {
EraseDestData(dst, "dirty");
}
}
}
}

bool CWallet::IsDirty(const uint256& hash, unsigned int n) const
{
const CWalletTx* srctx = GetWalletTx(hash);
if (srctx) {
CTxDestination dst;
if (ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst)) {
return ::IsMine(*this, dst) && GetDestData(dst, "dirty", nullptr);
}
}
return false;
}

bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
{
LOCK(cs_wallet);
Expand All @@ -930,6 +959,14 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)

uint256 hash = wtxIn.GetHash();

if (gArgs.GetBoolArg("-avoidreuse", DEFAULT_AVOIDREUSE)) {
// Mark used destinations as dirty
for (const CTxIn& txin : wtxIn.tx->vin) {
const COutPoint& op = txin.prevout;
SetDirtyState(op.hash, op.n, true);
}
}

// Inserts only if not already there, returns tx inserted or tx found
std::pair<std::map<uint256, CWalletTx>::iterator, bool> ret = mapWallet.insert(std::make_pair(hash, wtxIn));
CWalletTx& wtx = (*ret.first).second;
Expand Down Expand Up @@ -1938,7 +1975,7 @@ CAmount CWalletTx::GetImmatureCredit(bool fUseCache) const
return 0;
}

CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter) const
CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter, DestinationFilter dest_filter) const
{
if (pwallet == nullptr)
return 0;
Expand All @@ -1949,14 +1986,8 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter

CAmount* cache = nullptr;
bool* cache_used = nullptr;

if (filter == ISMINE_SPENDABLE) {
cache = &nAvailableCreditCached;
cache_used = &fAvailableCreditCached;
} else if (filter == ISMINE_WATCH_ONLY) {
cache = &nAvailableWatchCreditCached;
cache_used = &fAvailableWatchCreditCached;
}
if (filter == ISMINE_SPENDABLE) available_credit.Select(dest_filter, &cache_used, &cache);
else if (filter == ISMINE_WATCH_ONLY) available_watch_credit.Select(dest_filter, &cache_used, &cache);

if (fUseCache && cache_used && *cache_used) {
return *cache;
Expand All @@ -1966,7 +1997,7 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter
uint256 hashTx = GetHash();
for (unsigned int i = 0; i < tx->vout.size(); i++)
{
if (!pwallet->IsSpent(hashTx, i))
if (!pwallet->IsSpent(hashTx, i) && DestinationFilterApplies(dest_filter, pwallet->IsDirty(hashTx, i)))
{
const CTxOut &txout = tx->vout[i];
nCredit += pwallet->GetCredit(txout, filter);
Expand Down Expand Up @@ -2110,7 +2141,7 @@ void CWallet::ResendWalletTransactions(int64_t nBestBlockTime, CConnman* connman
*/


CAmount CWallet::GetBalance(const isminefilter& filter, const int min_depth) const
CAmount CWallet::GetBalance(const isminefilter& filter, const int min_depth, DestinationFilter dest_filter) const
{
CAmount nTotal = 0;
{
Expand All @@ -2119,7 +2150,7 @@ CAmount CWallet::GetBalance(const isminefilter& filter, const int min_depth) con
{
const CWalletTx* pcoin = &entry.second;
if (pcoin->IsTrusted() && pcoin->GetDepthInMainChain() >= min_depth) {
nTotal += pcoin->GetAvailableCredit(true, filter);
nTotal += pcoin->GetAvailableCredit(true, filter, dest_filter);
}
}
}
Expand Down Expand Up @@ -2250,6 +2281,7 @@ void CWallet::AvailableCoins(std::vector<COutput> &vCoins, bool fOnlySafe, const

vCoins.clear();
CAmount nTotal = 0;
DestinationFilter dest_filter = DeriveDestinationFilter(coinControl);

for (const auto& entry : mapWallet)
{
Expand Down Expand Up @@ -2330,6 +2362,10 @@ void CWallet::AvailableCoins(std::vector<COutput> &vCoins, bool fOnlySafe, const
continue;
}

if (!DestinationFilterApplies(dest_filter, IsDirty(wtxid, i))) {
continue;
}

bool solvable = IsSolvable(*this, pcoin->tx->vout[i].scriptPubKey);
bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable));

Expand Down Expand Up @@ -4454,3 +4490,11 @@ std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outpu
}
return groups;
}

DestinationFilter DeriveDestinationFilter(const CCoinControl* potential_coin_control)
{
if (potential_coin_control) return potential_coin_control->m_dest_filter;
return gArgs.GetBoolArg("-avoidreuse", DEFAULT_AVOIDREUSE)
? DestinationFilter::DestinationOnlyClean
: DestinationFilter::DestinationMixed;
}
Loading