Merge #13756: wallet: "avoid_reuse" wallet flag for improved privacy

5ebc6b0eb267e0552c66fffc5e5afe7df8becf80 bitcoind: update -avoidpartialspends description to account for auto-enable for avoid_reuse wallets (Karl-Johan Alm)
ada258f8c8f92d44d893cf9f22d15acdeca40b1a doc: release notes for avoid_reuse (Karl-Johan Alm)
27669551da52099e4a6a401acd7aa32b32832423 wallet: enable avoid_partial_spends by default if avoid_reuse is set (Karl-Johan Alm)
8f2e208f7c0468f9ba92bc789a698281b1c81284 test: add test for avoidreuse feature (Karl-Johan Alm)
0bdfbd34cf4015de87741ff549db35e5064f4e16 wallet/rpc: add 'avoid_reuse' option to RPC commands (Karl-Johan Alm)
f904723e0d5883309cb0dd14b826bc45c5e776fb wallet/rpc: add setwalletflag RPC and MUTABLE_WALLET_FLAGS (Karl-Johan Alm)
8247a0da3a46d7c38943ee0304343ab7465305bd wallet: enable avoid_reuse feature (Karl-Johan Alm)
eec15662fad917b169f5e3b8baaf4301dcf00a7b wallet: avoid reuse flags (Karl-Johan Alm)
58928098c299efdc7c5ddf2dc20716ca5272f21b wallet: make IsWalletFlagSet() const (Karl-Johan Alm)
129a5bafd9a3efa2fa16d780885048a06566d262 wallet: rename g_known_wallet_flags constant to KNOWN_WALLET_FLAGS (Karl-Johan Alm)

Pull request description:

  Add a new wallet flag called `avoid_reuse` which, when enabled, will keep track of when a specific destination has been spent from, and will actively "blacklist" any new UTXOs which send to an already-spent-from destination.

  This improves privacy, as a payer could otherwise begin tracking a payee's wallet by regularly peppering a known UTXO with dust outputs, which would then be scooped up and used in payments by the payee, allowing the payer to map out (1) the inputs owned by the payee and (2) the destinations to which the payee is making payments.

  This replaces #10386 and together with the (now merged) #12257 it addresses #10065 in full. The concerns raised in https://github.com/bitcoin/bitcoin/pull/10386#issuecomment-302361381 are also addressed due to #12257.

  ~~Note: this builds on top of #15780.~~ (merged)

ACKs for commit 5ebc6b:
  jnewbery:
    ACK 5ebc6b0eb
  laanwj:
    Concept and code-review ACK 5ebc6b0eb267e0552c66fffc5e5afe7df8becf80
  meshcollider:
    Code review ACK 5ebc6b0eb2
  achow101:
    ACK 5ebc6b0eb267e0552c66fffc5e5afe7df8becf80 modulo above nits

Tree-SHA512: fdef45826af544cbbb45634ac367852cc467ec87081d86d08b53ca849e588617e9a0a255b7e7bb28692d15332de58d6c3d274ac003355220e4213d7d9070742e
This commit is contained in:
MeshCollider 2019-06-19 11:32:02 +12:00 committed by Konstantin Akimov
parent 6e5440d505
commit 6099b4bf39
13 changed files with 480 additions and 35 deletions

View File

@ -0,0 +1,39 @@
Coin selection
--------------
### Reuse Avoidance
A new wallet flag `avoid_reuse` has been added (default off). When enabled,
a wallet will distinguish between used and unused addresses, and default to not
use the former in coin selection.
(Note: rescanning the blockchain is required, to correctly mark previously
used destinations.)
Together with "avoid partial spends" (present as of Bitcoin v0.17), this
addresses a serious privacy issue where a malicious user can track spends by
peppering a previously paid to address with near-dust outputs, which would then
be inadvertently included in future payments.
New RPCs
--------
- A new `setwalletflag` RPC sets/unsets flags for an existing wallet.
Updated RPCs
------------
Several RPCs have been updated to include an "avoid_reuse" flag, used to control
whether already used addresses should be left out or included in the operation.
These include:
- createwallet
- getbalance
- sendtoaddress
In addition, `sendtoaddress` has been changed to enable `-avoidpartialspends` when
`avoid_reuse` is enabled.
The listunspent RPC has also been updated to now include a "reused" bool, for nodes
with "avoid_reuse" enabled.

View File

@ -430,7 +430,7 @@ public:
CAmount getAvailableBalance(const CCoinControl& coin_control) override
{
if (coin_control.IsUsingCoinJoin()) {
return m_wallet->GetBalance(0, false, &coin_control).m_anonymized;
return m_wallet->GetBalance(0, coin_control.m_avoid_address_reuse, false, &coin_control).m_anonymized;
} else {
return m_wallet->GetAvailableBalance(&coin_control);
}

View File

@ -42,6 +42,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendtoaddress", 5, "use_is" },
{ "sendtoaddress", 6, "use_cj" },
{ "sendtoaddress", 7, "conf_target" },
{ "sendtoaddress", 9, "avoid_reuse" },
{ "settxfee", 0, "amount" },
{ "getreceivedbyaddress", 1, "minconf" },
{ "getreceivedbyaddress", 2, "addlocked" },
@ -59,6 +60,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "getbalance", 1, "minconf" },
{ "getbalance", 2, "addlocked" },
{ "getbalance", 3, "include_watchonly" },
{ "getbalance", 4, "avoid_reuse" },
{ "getchaintips", 0, "count" },
{ "getchaintips", 1, "branchlen" },
{ "getblockhash", 0, "height" },
@ -160,6 +162,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "setnetworkactive", 0, "state" },
{ "setcoinjoinrounds", 0, "rounds" },
{ "setcoinjoinamount", 0, "amount" },
{ "setwalletflag", 1, "value" },
{ "getmempoolancestors", 1, "verbose" },
{ "getmempooldescendants", 1, "verbose" },
{ "logging", 0, "include" },
@ -195,6 +198,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "rescanblockchain", 1, "stop_height"},
{ "createwallet", 1, "disable_private_keys"},
{ "createwallet", 2, "blank"},
{ "createwallet", 4, "avoid_reuse"},
{ "upgradetohd", 3, "rescan"},
{ "getnodeaddresses", 0, "count"},
{ "stop", 0, "wait" },

View File

@ -13,6 +13,7 @@ void CCoinControl::SetNull(bool fResetCoinType)
fAllowOtherInputs = false;
fAllowWatchOnly = false;
m_avoid_partial_spends = gArgs.GetBoolArg("-avoidpartialspends", DEFAULT_AVOIDPARTIALSPENDS);
m_avoid_address_reuse = false;
setSelected.clear();
m_feerate.reset();
fOverrideFeeRate = false;

View File

@ -46,6 +46,8 @@ public:
Optional<unsigned int> m_confirm_target;
//! Avoid partial use of funds sent to a given address
bool m_avoid_partial_spends;
//! Forbids inclusion of dirty (previously used) addresses
bool m_avoid_address_reuse;
//! Fee estimation mode to control arguments to estimateSmartFee
FeeEstimateMode m_fee_mode;
//! Minimum chain depth value for coin availability

View File

@ -44,7 +44,7 @@ const WalletInitInterface& g_wallet_init_interface = WalletInit();
void WalletInit::AddWalletOptions(ArgsManager& argsman) const
{
argsman.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), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
argsman.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 (always enabled for wallets with \"avoid_reuse\" enabled))", DEFAULT_AVOIDPARTIALSPENDS), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
argsman.AddArg("-createwalletbackups=<n>", strprintf("Number of automatic wallet backups (default: %u)", nWalletBackups), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
argsman.AddArg("-disablewallet", "Do not load the wallet and disable wallet RPC calls", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
#if HAVE_SYSTEM

View File

@ -20,7 +20,9 @@ enum isminetype : unsigned int
ISMINE_NO = 0,
ISMINE_WATCH_ONLY = 1 << 0,
ISMINE_SPENDABLE = 1 << 1,
ISMINE_USED = 1 << 2,
ISMINE_ALL = ISMINE_WATCH_ONLY | ISMINE_SPENDABLE,
ISMINE_ALL_USED = ISMINE_ALL | ISMINE_USED,
ISMINE_ENUM_ELEMENTS,
};
/** used for bitflags of isminetype */

View File

@ -49,6 +49,17 @@
static const std::string WALLET_ENDPOINT_BASE = "/wallet/";
static inline bool GetAvoidReuseFlag(CWallet * const pwallet, const UniValue& param) {
bool can_avoid_reuse = pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
bool avoid_reuse = param.isNull() ? can_avoid_reuse : param.get_bool();
if (avoid_reuse && !can_avoid_reuse) {
throw JSONRPCError(RPC_WALLET_ERROR, "wallet does not have the \"avoid reuse\" feature enabled");
}
return avoid_reuse;
}
bool GetWalletNameFromJSONRPCRequest(const JSONRPCRequest& request, std::string& wallet_name)
{
if (request.URI.substr(0, WALLET_ENDPOINT_BASE.size()) == WALLET_ENDPOINT_BASE) {
@ -280,7 +291,7 @@ static UniValue setlabel(const JSONRPCRequest& request)
static CTransactionRef SendMoney(CWallet* const pwallet, const CTxDestination& address, CAmount nValue, bool fSubtractFeeFromAmount, const CCoinControl& coin_control, mapValue_t mapValue)
{
CAmount curBalance = pwallet->GetBalance().m_mine_trusted;
CAmount curBalance = pwallet->GetBalance(0, coin_control.m_avoid_address_reuse).m_mine_trusted;
// Check amount
if (nValue <= 0)
@ -319,6 +330,10 @@ static CTransactionRef SendMoney(CWallet* const pwallet, const CTxDestination& a
static UniValue sendtoaddress(const JSONRPCRequest& request)
{
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
CWallet* const pwallet = wallet.get();
RPCHelpMan{"sendtoaddress",
"\nSend an amount to a given address." +
HelpRequiringPassphrase() + "\n",
@ -339,6 +354,8 @@ static UniValue sendtoaddress(const JSONRPCRequest& request)
" \"UNSET\"\n"
" \"ECONOMICAL\"\n"
" \"CONSERVATIVE\""},
{"avoid_reuse", RPCArg::Type::BOOL, /* default */ pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) ? "true" : "unavailable", "Avoid spending from dirty addresses; addresses are considered\n"
" dirty if they have previously been used in a transaction."},
},
RPCResult{
RPCResult::Type::STR_HEX, "txid", "The transaction id."
@ -351,10 +368,6 @@ static UniValue sendtoaddress(const JSONRPCRequest& request)
},
}.Check(request);
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
CWallet* const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent block
// the user could have gotten from another RPC command prior to now
pwallet->BlockUntilSyncedToCurrentChain();
@ -399,6 +412,10 @@ static UniValue sendtoaddress(const JSONRPCRequest& request)
}
}
coin_control.m_avoid_address_reuse = GetAvoidReuseFlag(pwallet, request.params[9]);
// We also enable partial spend avoidance if reuse avoidance is set.
coin_control.m_avoid_partial_spends |= coin_control.m_avoid_address_reuse;
EnsureWalletIsUnlocked(pwallet);
CTransactionRef tx = SendMoney(pwallet, dest, nAmount, fSubtractFeeFromAmount, coin_control, std::move(mapValue));
@ -711,6 +728,10 @@ static UniValue getreceivedbylabel(const JSONRPCRequest& request)
static UniValue getbalance(const JSONRPCRequest& request)
{
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
CWallet* const pwallet = wallet.get();
RPCHelpMan{"getbalance",
"\nReturns the total available balance.\n"
"The available balance is what the wallet considers currently spendable, and is\n"
@ -720,6 +741,7 @@ static UniValue getbalance(const JSONRPCRequest& request)
{"minconf", RPCArg::Type::NUM, /* default */ "0", "Only include transactions confirmed at least this many times."},
{"addlocked", RPCArg::Type::BOOL, /* default */ "false", "Whether to include transactions locked via InstantSend in the wallet's balance."},
{"include_watchonly", RPCArg::Type::BOOL, /* default */ "false", "Also include balance in watch-only addresses (see 'importaddress')"},
{"avoid_reuse", RPCArg::Type::BOOL, /* default */ pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) ? "true" : "unavailable", "Do not include balance in dirty outputs; addresses are considered dirty if they have previously been used in a transaction."},
},
RPCResult{
RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received for this wallet."
@ -734,10 +756,6 @@ static UniValue getbalance(const JSONRPCRequest& request)
},
}.Check(request);
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
CWallet* const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent block
// the user could have gotten from another RPC command prior to now
pwallet->BlockUntilSyncedToCurrentChain();
@ -765,7 +783,8 @@ static UniValue getbalance(const JSONRPCRequest& request)
include_watchonly = true;
}
const auto bal = pwallet->GetBalance(min_depth, fAddLocked);
bool avoid_reuse = GetAvoidReuseFlag(pwallet, request.params[4]);
const auto bal = pwallet->GetBalance(min_depth, avoid_reuse, fAddLocked);
return ValueFromAmount(bal.m_mine_trusted + (include_watchonly ? bal.m_watchonly_trusted : 0));
}
@ -2414,6 +2433,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
{RPCResult::Type::NUM, "hdinternalkeyindex", "current internal childkey index"},
}},
}},
{RPCResult::Type::BOOL, "avoid_reuse", "whether this wallet tracks clean/dirty coins in terms of reuse"},
{RPCResult::Type::OBJ, "scanning", "current scanning details, or false if no scan is in progress",
{
{RPCResult::Type::NUM, "duration", "elapsed seconds since scan start"},
@ -2477,6 +2497,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
}
obj.pushKV("hdaccounts", accounts);
}
obj.pushKV("avoid_reuse", pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE));
if (pwallet->IsScanning()) {
UniValue scanning(UniValue::VOBJ);
scanning.pushKV("duration", pwallet->ScanningDuration() / 1000);
@ -2703,6 +2724,72 @@ static UniValue loadwallet(const JSONRPCRequest& request)
return obj;
}
static UniValue setwalletflag(const JSONRPCRequest& request)
{
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
CWallet* const pwallet = wallet.get();
std::string flags = "";
for (auto& it : WALLET_FLAG_MAP)
if (it.second & MUTABLE_WALLET_FLAGS)
flags += (flags == "" ? "" : ", ") + it.first;
RPCHelpMan{"setwalletflag",
"\nChange the state of the given wallet flag for a wallet.\n",
{
{"flag", RPCArg::Type::STR, RPCArg::Optional::NO, "The name of the flag to change. Current available flags: " + flags},
{"value", RPCArg::Type::BOOL, /* default */ "true", "The new state."},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR, "flag_name", "The name of the flag that was modified"},
{RPCResult::Type::BOOL, "flag_state", "The new state of the flag"},
{RPCResult::Type::STR, "warnings", "Any warnings associated with the change"},
}
},
RPCExamples{
HelpExampleCli("setwalletflag", "avoid_reuse")
+ HelpExampleRpc("setwalletflag", "\"avoid_reuse\"")
},
}.Check(request);
std::string flag_str = request.params[0].get_str();
bool value = request.params[1].isNull() || request.params[1].get_bool();
if (!WALLET_FLAG_MAP.count(flag_str)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unknown wallet flag: %s", flag_str));
}
auto flag = WALLET_FLAG_MAP.at(flag_str);
if (!(flag & MUTABLE_WALLET_FLAGS)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Wallet flag is immutable: %s", flag_str));
}
UniValue res(UniValue::VOBJ);
if (pwallet->IsWalletFlagSet(flag) == value) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Wallet flag is already set to %s: %s", value ? "true" : "false", flag_str));
}
res.pushKV("flag_name", flag_str);
res.pushKV("flag_state", value);
if (value) {
pwallet->SetWalletFlag(flag);
} else {
pwallet->UnsetWalletFlag(flag);
}
if (flag && value && WALLET_FLAG_CAVEATS.count(flag)) {
res.pushKV("warnings", WALLET_FLAG_CAVEATS.at(flag));
}
return res;
}
static UniValue createwallet(const JSONRPCRequest& request)
{
RPCHelpMan{
@ -2713,6 +2800,7 @@ static UniValue createwallet(const JSONRPCRequest& request)
{"disable_private_keys", RPCArg::Type::BOOL, /* default */ "false", "Disable the possibility of private keys (only watchonlys are possible in this mode)."},
{"blank", RPCArg::Type::BOOL, /* default */ "false", "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using sethdseed."},
{"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."},
{"avoid_reuse", RPCArg::Type::BOOL, /* default */ "false", "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
@ -2747,6 +2835,10 @@ static UniValue createwallet(const JSONRPCRequest& request)
}
}
if (!request.params[4].isNull() && request.params[4].get_bool()) {
flags |= WALLET_FLAG_AVOID_REUSE;
}
bilingual_str error;
std::shared_ptr<CWallet> wallet;
WalletCreationStatus status = CreateWallet(*context.chain, passphrase, flags, request.params[0].get_str(), error, warnings, wallet);
@ -2810,6 +2902,11 @@ static UniValue unloadwallet(const JSONRPCRequest& request)
static UniValue listunspent(const JSONRPCRequest& request)
{
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
CWallet* const pwallet = wallet.get();
bool avoid_reuse = pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
RPCHelpMan{"listunspent",
"\nReturns array of unspent transaction outputs\n"
"with between minconf and maxconf (inclusive) confirmations.\n"
@ -2852,6 +2949,7 @@ static UniValue listunspent(const JSONRPCRequest& request)
{RPCResult::Type::BOOL, "spendable", "Whether we have the private keys to spend this output"},
{RPCResult::Type::BOOL, "solvable", "Whether we know how to spend this output, ignoring the lack of keys"},
{RPCResult::Type::STR, "desc", "(only when solvable) A descriptor for spending this output"},
{RPCResult::Type::BOOL, "reused", /* optional*/ true, "Whether this output is reused/dirty (sent to an address that was previously spent from)"},
{RPCResult::Type::BOOL, "safe", "Whether this output is considered safe to spend. Unconfirmed transactions"
" from outside keys and unconfirmed replacement transactions are considered unsafe\n"
"and are not eligible for spending by fundrawtransaction and sendtoaddress."},
@ -2867,10 +2965,6 @@ static UniValue listunspent(const JSONRPCRequest& request)
},
}.Check(request);
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
CWallet* const pwallet = wallet.get();
int nMinDepth = 1;
if (!request.params[0].isNull()) {
RPCTypeCheckArgument(request.params[0], UniValue::VNUM);
@ -2960,6 +3054,8 @@ static UniValue listunspent(const JSONRPCRequest& request)
UniValue results(UniValue::VARR);
std::vector<COutput> vecOutputs;
{
coinControl.m_avoid_address_reuse = false;
LOCK(pwallet->cs_wallet);
pwallet->AvailableCoins(vecOutputs, !include_unsafe, &coinControl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount, nMinDepth, nMaxDepth);
}
@ -2970,6 +3066,7 @@ static UniValue listunspent(const JSONRPCRequest& request)
CTxDestination address;
const CScript& scriptPubKey = out.tx->tx->vout[out.i].scriptPubKey;
bool fValidAddress = ExtractDestination(scriptPubKey, address);
bool reused = avoid_reuse && pwallet->IsUsedDestination(address);
if (destinations.size() && (!fValidAddress || !destinations.count(address)))
continue;
@ -3004,6 +3101,7 @@ static UniValue listunspent(const JSONRPCRequest& request)
auto descriptor = InferDescriptor(scriptPubKey, *pwallet);
entry.pushKV("desc", descriptor->ToString());
}
if (avoid_reuse) entry.pushKV("reused", reused);
entry.pushKV("safe", out.fSafe);
entry.pushKV("coinjoin_rounds", pwallet->GetRealOutpointCoinJoinRounds(COutPoint(out.tx->GetHash(), out.i)));
results.push_back(entry);
@ -3872,14 +3970,14 @@ static const CRPCCommand commands[] =
{ "wallet", "abortrescan", &abortrescan, {} },
{ "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label"} },
{ "wallet", "backupwallet", &backupwallet, {"destination"} },
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase"} },
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse"} },
{ "wallet", "dumphdinfo", &dumphdinfo, {} },
{ "wallet", "dumpprivkey", &dumpprivkey, {"address"} },
{ "wallet", "dumpwallet", &dumpwallet, {"filename"} },
{ "wallet", "encryptwallet", &encryptwallet, {"passphrase"} },
{ "wallet", "getaddressesbylabel", &getaddressesbylabel, {"label"} },
{ "wallet", "getaddressinfo", &getaddressinfo, {"address"} },
{ "wallet", "getbalance", &getbalance, {"dummy","minconf","addlocked","include_watchonly"} },
{ "wallet", "getbalance", &getbalance, {"dummy","minconf","addlocked","include_watchonly", "avoid_reuse"} },
{ "wallet", "getnewaddress", &getnewaddress, {"label"} },
{ "wallet", "getrawchangeaddress", &getrawchangeaddress, {} },
{ "wallet", "getreceivedbyaddress", &getreceivedbyaddress, {"address","minconf","addlocked"} },
@ -3911,11 +4009,12 @@ static const CRPCCommand commands[] =
{ "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} },
{ "wallet", "rescanblockchain", &rescanblockchain, {"start_height", "stop_height"} },
{ "wallet", "sendmany", &sendmany, {"dummy","amounts","minconf","addlocked","comment","subtractfeefrom","use_is","use_cj","conf_target","estimate_mode"} },
{ "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","use_is","use_cj","conf_target","estimate_mode"} },
{ "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","use_is","use_cj","conf_target","estimate_mode", "avoid_reuse"} },
{ "wallet", "setcoinjoinrounds", &setcoinjoinrounds, {"rounds"} },
{ "wallet", "setcoinjoinamount", &setcoinjoinamount, {"amount"} },
{ "wallet", "setlabel", &setlabel, {"address","label"} },
{ "wallet", "settxfee", &settxfee, {"amount"} },
{ "wallet", "setwalletflag", &setwalletflag, {"flag","value"} },
{ "wallet", "signmessage", &signmessage, {"address","message"} },
{ "wallet", "signrawtransactionwithwallet", &signrawtransactionwithwallet, {"hexstring","prevtxs","sighashtype"} },
{ "wallet", "unloadwallet", &unloadwallet, {"wallet_name"} },

View File

@ -53,6 +53,14 @@
#include <boost/algorithm/string/replace.hpp>
const std::map<uint64_t,std::string> WALLET_FLAG_CAVEATS{
{WALLET_FLAG_AVOID_REUSE,
"You need to rescan the blockchain in order to correctly mark used "
"destinations in the past. Until this is done, some destinations may "
"be considered unused, even if the opposite is the case."
},
};
static const size_t OUTPUT_GROUP_MAX_ENTRIES = 10;
static CCriticalSection cs_wallets;
@ -1178,6 +1186,37 @@ void CWallet::MarkDirty()
fAnonymizableTallyCachedNonDenom = false;
}
void CWallet::SetUsedDestinationState(const uint256& hash, unsigned int n, bool used)
{
const CWalletTx* srctx = GetWalletTx(hash);
if (!srctx) return;
CTxDestination dst;
if (ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst)) {
if (::IsMine(*this, dst)) {
LOCK(cs_wallet);
if (used && !GetDestData(dst, "used", nullptr)) {
AddDestData(dst, "used", "p"); // p for "present", opposite of absent (null)
} else if (!used && GetDestData(dst, "used", nullptr)) {
EraseDestData(dst, "used");
}
}
}
}
bool CWallet::IsUsedDestination(const CTxDestination& dst) const
{
LOCK(cs_wallet);
return ::IsMine(*this, dst) && GetDestData(dst, "used", nullptr);
}
bool CWallet::IsUsedDestination(const uint256& hash, unsigned int n) const
{
CTxDestination dst;
const CWalletTx* srctx = GetWalletTx(hash);
return srctx && ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst) && IsUsedDestination(dst);
}
bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
{
LOCK(cs_wallet);
@ -1186,6 +1225,14 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
uint256 hash = wtxIn.GetHash();
if (IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) {
// Mark used destinations
for (const CTxIn& txin : wtxIn.tx->vin) {
const COutPoint& op = txin.prevout;
SetUsedDestinationState(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;
@ -2122,7 +2169,7 @@ void CWallet::UnsetWalletFlag(WalletBatch& batch, uint64_t flag)
throw std::runtime_error(std::string(__func__) + ": writing wallet flags failed");
}
bool CWallet::IsWalletFlagSet(uint64_t flag)
bool CWallet::IsWalletFlagSet(uint64_t flag) const
{
return (m_wallet_flags & flag);
}
@ -2131,7 +2178,7 @@ bool CWallet::SetWalletFlags(uint64_t overwriteFlags, bool memonly)
{
LOCK(cs_wallet);
m_wallet_flags = overwriteFlags;
if (((overwriteFlags & g_known_wallet_flags) >> 32) ^ (overwriteFlags >> 32)) {
if (((overwriteFlags & KNOWN_WALLET_FLAGS) >> 32) ^ (overwriteFlags >> 32)) {
// contains unknown non-tolerable wallet flags
return false;
}
@ -2631,7 +2678,7 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter
return 0;
// Avoid caching ismine for NO or ALL cases (could remove this check and simplify in the future).
bool allow_cache = filter == ISMINE_SPENDABLE || filter == ISMINE_WATCH_ONLY;
bool allow_cache = (filter & ISMINE_ALL) && (filter & ISMINE_ALL) != ISMINE_ALL;
// Must wait until coinbase is safely deep enough in the chain before valuing it
if (IsImmatureCoinBase())
@ -2641,11 +2688,12 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter
return m_amounts[AVAILABLE_CREDIT].m_value[filter];
}
bool allow_used_addresses = (filter & ISMINE_USED) || !pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
CAmount nCredit = 0;
uint256 hashTx = GetHash();
for (unsigned int i = 0; i < tx->vout.size(); i++)
{
if (!pwallet->IsSpent(hashTx, i))
if (!pwallet->IsSpent(hashTx, i) && (allow_used_addresses || !pwallet->IsUsedDestination(hashTx, i)))
{
const CTxOut &txout = tx->vout[i];
nCredit += pwallet->GetCredit(txout, filter);
@ -2899,16 +2947,17 @@ std::unordered_set<const CWalletTx*, WalletTxHasher> CWallet::GetSpendableTXs()
return ret;
}
CWallet::Balance CWallet::GetBalance(const int min_depth, const bool fAddLocked, const CCoinControl* coinControl) const
CWallet::Balance CWallet::GetBalance(const int min_depth, const bool avoid_reuse, const bool fAddLocked, const CCoinControl* coinControl) const
{
Balance ret;
isminefilter reuse_filter = avoid_reuse ? 0 : ISMINE_USED;
{
LOCK(cs_wallet);
for (auto pcoin : GetSpendableTXs()) {
const bool is_trusted{pcoin->IsTrusted()};
const int tx_depth{pcoin->GetDepthInMainChain()};
const CAmount tx_credit_mine{pcoin->GetAvailableCredit(/* fUseCache */ true, ISMINE_SPENDABLE)};
const CAmount tx_credit_watchonly{pcoin->GetAvailableCredit(/* fUseCache */ true, ISMINE_WATCH_ONLY)};
const CAmount tx_credit_mine{pcoin->GetAvailableCredit(/* fUseCache */ true, ISMINE_SPENDABLE | reuse_filter)};
const CAmount tx_credit_watchonly{pcoin->GetAvailableCredit(/* fUseCache */ true, ISMINE_WATCH_ONLY | reuse_filter)};
if (is_trusted && ((tx_depth >= min_depth) || (fAddLocked && pcoin->IsLockedByInstantSend()))) {
ret.m_mine_trusted += tx_credit_mine;
ret.m_watchonly_trusted += tx_credit_watchonly;
@ -3020,6 +3069,9 @@ void CWallet::AvailableCoins(std::vector<COutput> &vCoins, bool fOnlySafe, const
CoinType nCoinType = coinControl ? coinControl->nCoinType : CoinType::ALL_COINS;
CAmount nTotal = 0;
// Either the WALLET_FLAG_AVOID_REUSE flag is not set (in which case we always allow), or we default to avoiding, and only in the case where
// a coin control object is provided, and has the avoid address reuse flag set to false, do we allow already used addresses
bool allow_used_addresses = !IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) || (coinControl && !coinControl->m_avoid_address_reuse);
for (auto pcoin : GetSpendableTXs()) {
const uint256& wtxid = pcoin->GetHash();
@ -3084,6 +3136,9 @@ void CWallet::AvailableCoins(std::vector<COutput> &vCoins, bool fOnlySafe, const
continue;
}
if (!allow_used_addresses && IsUsedDestination(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));
@ -5071,12 +5126,8 @@ std::shared_ptr<CWallet> CWallet::CreateWalletFromFile(interfaces::Chain& chain,
if (fFirstRun)
{
if ((wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
//selective allow to set flags
walletInstance->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
} else if (wallet_creation_flags & WALLET_FLAG_BLANK_WALLET) {
walletInstance->SetWalletFlag(WALLET_FLAG_BLANK_WALLET);
} else {
walletInstance->SetWalletFlags(wallet_creation_flags, false);
if (!(wallet_creation_flags & (WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET))) {
// Create new HD chain
if (gArgs.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET) && !walletInstance->IsHDEnabled()) {
std::string strSeed = gArgs.GetArg("-hdseed", "not hex");

View File

@ -132,6 +132,10 @@ enum WalletFlags : uint64_t {
// wallet flags in the upper section (> 1 << 31) will lead to not opening the wallet if flag is unknown
// unknown wallet flags in the lower section <= (1 << 31) will be tolerated
// will categorize coins as clean (not reused) and dirty (reused), and handle
// them with privacy considerations in mind
WALLET_FLAG_AVOID_REUSE = (1ULL << 0),
// Indicates that the metadata has already been upgraded to contain key origins
WALLET_FLAG_KEY_ORIGIN_METADATA = (1ULL << 1),
@ -151,7 +155,23 @@ enum WalletFlags : uint64_t {
WALLET_FLAG_BLANK_WALLET = (1ULL << 33),
};
static constexpr uint64_t g_known_wallet_flags = WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET | WALLET_FLAG_KEY_ORIGIN_METADATA;
static constexpr uint64_t KNOWN_WALLET_FLAGS =
WALLET_FLAG_AVOID_REUSE
| WALLET_FLAG_BLANK_WALLET
| WALLET_FLAG_KEY_ORIGIN_METADATA
| WALLET_FLAG_DISABLE_PRIVATE_KEYS;
static constexpr uint64_t MUTABLE_WALLET_FLAGS =
WALLET_FLAG_AVOID_REUSE;
static const std::map<std::string,WalletFlags> WALLET_FLAG_MAP{
{"avoid_reuse", WALLET_FLAG_AVOID_REUSE},
{"blank", WALLET_FLAG_BLANK_WALLET},
{"key_origin_metadata", WALLET_FLAG_KEY_ORIGIN_METADATA},
{"disable_private_keys", WALLET_FLAG_DISABLE_PRIVATE_KEYS},
};
extern const std::map<uint64_t,std::string> WALLET_FLAG_CAVEATS;
/** A key from a CWallet's keypool
*
@ -1005,6 +1025,12 @@ public:
bool IsFullyMixed(const COutPoint& outpoint) const;
bool IsSpent(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
// Whether this or any UTXO with the same CTxDestination has been spent.
bool IsUsedDestination(const CTxDestination& dst) const;
bool IsUsedDestination(const uint256& hash, unsigned int n) const;
void SetUsedDestinationState(const uint256& hash, unsigned int n, bool used);
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, bool single_coin) const;
bool IsLockedCoin(uint256 hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
@ -1144,7 +1170,7 @@ public:
CAmount m_denominated_trusted{0};
CAmount m_denominated_untrusted_pending{0};
};
Balance GetBalance(int min_depth = 0, const bool fAddLocked = false, const CCoinControl* coinControl = nullptr) const;
Balance GetBalance(const int min_depth = 0, const bool avoid_reuse = true, const bool fAddLocked = false, const CCoinControl* coinControl = nullptr) const;
CAmount GetAnonymizableBalance(bool fSkipDenominated = false, bool fSkipUnconfirmed = true) const;
float GetAverageAnonymizedRounds() const;
@ -1420,7 +1446,7 @@ public:
void UnsetWalletFlag(WalletBatch& batch, uint64_t flag);
/** check if a certain wallet flag is set */
bool IsWalletFlagSet(uint64_t flag);
bool IsWalletFlagSet(uint64_t flag) const;
/** overwrite all flags by the given uint64_t
returns false if unknown, non-tolerable flags are present */

View File

@ -141,6 +141,7 @@ BASE_SCRIPTS = [
'rpc_misc.py',
'interface_rest.py',
'mempool_spend_coinbase.py',
'wallet_avoidreuse.py',
'mempool_reorg.py',
'mempool_persist.py',
'wallet_multiwallet.py',

View File

@ -0,0 +1,217 @@
#!/usr/bin/env python3
# Copyright (c) 2018 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the avoid_reuse and setwalletflag features."""
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
connect_nodes,
)
# TODO: Copied from wallet_groups.py -- should perhaps move into util.py
def assert_approx(v, vexp, vspan=0.00001):
if v < vexp - vspan:
raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
if v > vexp + vspan:
raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
def reset_balance(node, discardaddr):
'''Throw away all owned coins by the node so it gets a balance of 0.'''
balance = node.getbalance(avoid_reuse=False)
if balance > 0.5:
node.sendtoaddress(address=discardaddr, amount=balance, subtractfeefromamount=True, avoid_reuse=False)
def count_unspent(node):
'''Count the unspent outputs for the given node and return various statistics'''
r = {
"total": {
"count": 0,
"sum": 0,
},
"reused": {
"count": 0,
"sum": 0,
},
}
supports_reused = True
for utxo in node.listunspent(minconf=0):
r["total"]["count"] += 1
r["total"]["sum"] += utxo["amount"]
if supports_reused and "reused" in utxo:
if utxo["reused"]:
r["reused"]["count"] += 1
r["reused"]["sum"] += utxo["amount"]
else:
supports_reused = False
r["reused"]["supported"] = supports_reused
return r
def assert_unspent(node, total_count=None, total_sum=None, reused_supported=None, reused_count=None, reused_sum=None):
'''Make assertions about a node's unspent output statistics'''
stats = count_unspent(node)
if total_count is not None:
assert_equal(stats["total"]["count"], total_count)
if total_sum is not None:
assert_approx(stats["total"]["sum"], total_sum, 0.001)
if reused_supported is not None:
assert_equal(stats["reused"]["supported"], reused_supported)
if reused_count is not None:
assert_equal(stats["reused"]["count"], reused_count)
if reused_sum is not None:
assert_approx(stats["reused"]["sum"], reused_sum, 0.001)
class AvoidReuseTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = False
self.num_nodes = 2
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def run_test(self):
'''Set up initial chain and run tests defined below'''
self.test_persistence()
self.test_immutable()
self.nodes[0].generate(110)
self.sync_all()
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
self.test_fund_send_fund_senddirty()
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
self.test_fund_send_fund_send()
def test_persistence(self):
'''Test that wallet files persist the avoid_reuse flag.'''
# Configure node 1 to use avoid_reuse
self.nodes[1].setwalletflag('avoid_reuse')
# Flags should be node1.avoid_reuse=false, node2.avoid_reuse=true
assert_equal(self.nodes[0].getwalletinfo()["avoid_reuse"], False)
assert_equal(self.nodes[1].getwalletinfo()["avoid_reuse"], True)
# Stop and restart node 1
self.stop_node(1)
self.start_node(1)
connect_nodes(self.nodes[0], 1)
# Flags should still be node1.avoid_reuse=false, node2.avoid_reuse=true
assert_equal(self.nodes[0].getwalletinfo()["avoid_reuse"], False)
assert_equal(self.nodes[1].getwalletinfo()["avoid_reuse"], True)
# Attempting to set flag to its current state should throw
assert_raises_rpc_error(-8, "Wallet flag is already set to false", self.nodes[0].setwalletflag, 'avoid_reuse', False)
assert_raises_rpc_error(-8, "Wallet flag is already set to true", self.nodes[1].setwalletflag, 'avoid_reuse', True)
def test_immutable(self):
'''Test immutable wallet flags'''
# Attempt to set the disable_private_keys flag; this should not work
assert_raises_rpc_error(-8, "Wallet flag is immutable", self.nodes[1].setwalletflag, 'disable_private_keys')
tempwallet = ".wallet_avoidreuse.py_test_immutable_wallet.dat"
# Create a wallet with disable_private_keys set; this should work
self.nodes[1].createwallet(tempwallet, True)
w = self.nodes[1].get_wallet_rpc(tempwallet)
# Attempt to unset the disable_private_keys flag; this should not work
assert_raises_rpc_error(-8, "Wallet flag is immutable", w.setwalletflag, 'disable_private_keys', False)
# Unload temp wallet
self.nodes[1].unloadwallet(tempwallet)
def test_fund_send_fund_senddirty(self):
'''
Test the same as test_fund_send_fund_send, except send the 10 BTC with
the avoid_reuse flag set to false. This means the 10 BTC send should succeed,
where it fails in test_fund_send_fund_send.
'''
fundaddr = self.nodes[1].getnewaddress()
retaddr = self.nodes[0].getnewaddress()
self.nodes[0].sendtoaddress(fundaddr, 10)
self.nodes[0].generate(1)
self.sync_all()
# listunspent should show 1 single, unused 10 btc output
assert_unspent(self.nodes[1], total_count=1, total_sum=10, reused_supported=True, reused_count=0)
self.nodes[1].sendtoaddress(retaddr, 5)
self.nodes[0].generate(1)
self.sync_all()
# listunspent should show 1 single, unused 5 btc output
assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_supported=True, reused_count=0)
self.nodes[0].sendtoaddress(fundaddr, 10)
self.nodes[0].generate(1)
self.sync_all()
# listunspent should show 2 total outputs (5, 10 btc), one unused (5), one reused (10)
assert_unspent(self.nodes[1], total_count=2, total_sum=15, reused_count=1, reused_sum=10)
self.nodes[1].sendtoaddress(address=retaddr, amount=10, avoid_reuse=False)
# listunspent should show 1 total outputs (5 btc), unused
assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_count=0)
# node 1 should now have about 5 btc left (for both cases)
assert_approx(self.nodes[1].getbalance(), 5, 0.001)
assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 5, 0.001)
def test_fund_send_fund_send(self):
'''
Test the simple case where [1] generates a new address A, then
[0] sends 10 BTC to A.
[1] spends 5 BTC from A. (leaving roughly 5 BTC useable)
[0] sends 10 BTC to A again.
[1] tries to spend 10 BTC (fails; dirty).
[1] tries to spend 4 BTC (succeeds; change address sufficient)
'''
fundaddr = self.nodes[1].getnewaddress()
retaddr = self.nodes[0].getnewaddress()
self.nodes[0].sendtoaddress(fundaddr, 10)
self.nodes[0].generate(1)
self.sync_all()
# listunspent should show 1 single, unused 10 btc output
assert_unspent(self.nodes[1], total_count=1, total_sum=10, reused_supported=True, reused_count=0)
self.nodes[1].sendtoaddress(retaddr, 5)
self.nodes[0].generate(1)
self.sync_all()
# listunspent should show 1 single, unused 5 btc output
assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_supported=True, reused_count=0)
self.nodes[0].sendtoaddress(fundaddr, 10)
self.nodes[0].generate(1)
self.sync_all()
# listunspent should show 2 total outputs (5, 10 btc), one unused (5), one reused (10)
assert_unspent(self.nodes[1], total_count=2, total_sum=15, reused_count=1, reused_sum=10)
# node 1 should now have a balance of 5 (no dirty) or 15 (including dirty)
assert_approx(self.nodes[1].getbalance(), 5, 0.001)
assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 15, 0.001)
assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[1].sendtoaddress, retaddr, 10)
self.nodes[1].sendtoaddress(retaddr, 4)
# listunspent should show 2 total outputs (1, 10 btc), one unused (1), one reused (10)
assert_unspent(self.nodes[1], total_count=2, total_sum=11, reused_count=1, reused_sum=10)
# node 1 should now have about 1 btc left (no dirty) and 11 (including dirty)
assert_approx(self.nodes[1].getbalance(), 1, 0.001)
assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 11, 0.001)
if __name__ == '__main__':
AvoidReuseTest().main()

View File

@ -79,6 +79,9 @@ class WalletTest(BitcoinTestFramework):
assert_equal(self.nodes[0].getbalance("*"), 500)
assert_equal(self.nodes[0].getbalance("*", 1), 500)
assert_equal(self.nodes[0].getbalance("*", 1, True), 500)
assert_equal(self.nodes[0].getbalance("*", 1, True, False), 500)
assert_equal(self.nodes[0].getbalance(minconf=1, addlocked=True), 500)
assert_equal(self.nodes[0].getbalance(minconf=1, avoid_reuse=False), 500)
assert_equal(self.nodes[0].getbalance(minconf=1), 500)
assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 1000)
assert_equal(self.nodes[1].getbalance(minconf=0, include_watchonly=True), 500)