diff options
author | MeshCollider <dobsonsa68@gmail.com> | 2019-06-19 11:32:02 +1200 |
---|---|---|
committer | MeshCollider <dobsonsa68@gmail.com> | 2019-06-19 11:33:03 +1200 |
commit | 44d81723236114f9370f386f3b3310477a6dde43 (patch) | |
tree | e807263ade169f48543f0d91c66d4e305b7a9cae | |
parent | ac4d38c337dbae58a5ab712d61da1db7a9491fc2 (diff) | |
parent | 5ebc6b0eb267e0552c66fffc5e5afe7df8becf80 (diff) |
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 https://github.com/bitcoin/bitcoin/pull/13756/commits/5ebc6b0eb267e0552c66fffc5e5afe7df8becf80
achow101:
ACK 5ebc6b0eb267e0552c66fffc5e5afe7df8becf80 modulo above nits
Tree-SHA512: fdef45826af544cbbb45634ac367852cc467ec87081d86d08b53ca849e588617e9a0a255b7e7bb28692d15332de58d6c3d274ac003355220e4213d7d9070742e
-rw-r--r-- | doc/release-notes-13756.md | 39 | ||||
-rw-r--r-- | src/rpc/client.cpp | 4 | ||||
-rw-r--r-- | src/script/ismine.h | 2 | ||||
-rw-r--r-- | src/wallet/coincontrol.cpp | 1 | ||||
-rw-r--r-- | src/wallet/coincontrol.h | 2 | ||||
-rw-r--r-- | src/wallet/init.cpp | 2 | ||||
-rw-r--r-- | src/wallet/rpcwallet.cpp | 122 | ||||
-rw-r--r-- | src/wallet/wallet.cpp | 81 | ||||
-rw-r--r-- | src/wallet/wallet.h | 32 | ||||
-rwxr-xr-x | test/functional/test_runner.py | 1 | ||||
-rwxr-xr-x | test/functional/wallet_avoidreuse.py | 217 |
11 files changed, 476 insertions, 27 deletions
diff --git a/doc/release-notes-13756.md b/doc/release-notes-13756.md new file mode 100644 index 0000000000..21006f46a0 --- /dev/null +++ b/doc/release-notes-13756.md @@ -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. diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 4144a17bc3..8f6bdc21aa 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -36,6 +36,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "sendtoaddress", 4, "subtractfeefromamount" }, { "sendtoaddress", 5 , "replaceable" }, { "sendtoaddress", 6 , "conf_target" }, + { "sendtoaddress", 8, "avoid_reuse" }, { "settxfee", 0, "amount" }, { "sethdseed", 0, "newkeypool" }, { "getreceivedbyaddress", 1, "minconf" }, @@ -48,6 +49,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "listreceivedbylabel", 2, "include_watchonly" }, { "getbalance", 1, "minconf" }, { "getbalance", 2, "include_watchonly" }, + { "getbalance", 3, "avoid_reuse" }, { "getblockhash", 0, "height" }, { "waitforblockheight", 0, "height" }, { "waitforblockheight", 1, "timeout" }, @@ -141,6 +143,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "setban", 2, "bantime" }, { "setban", 3, "absolute" }, { "setnetworkactive", 0, "state" }, + { "setwalletflag", 1, "value" }, { "getmempoolancestors", 1, "verbose" }, { "getmempooldescendants", 1, "verbose" }, { "bumpfee", 1, "options" }, @@ -162,6 +165,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "rescanblockchain", 1, "stop_height"}, { "createwallet", 1, "disable_private_keys"}, { "createwallet", 2, "blank"}, + { "createwallet", 4, "avoid_reuse"}, { "getnodeaddresses", 0, "count"}, { "stop", 0, "wait" }, }; diff --git a/src/script/ismine.h b/src/script/ismine.h index 55e28e225a..da3da7e324 100644 --- a/src/script/ismine.h +++ b/src/script/ismine.h @@ -20,7 +20,9 @@ enum isminetype 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 */ diff --git a/src/wallet/coincontrol.cpp b/src/wallet/coincontrol.cpp index 87d2c4f06e..60bce66839 100644 --- a/src/wallet/coincontrol.cpp +++ b/src/wallet/coincontrol.cpp @@ -13,6 +13,7 @@ void CCoinControl::SetNull() 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; diff --git a/src/wallet/coincontrol.h b/src/wallet/coincontrol.h index 12ba032dff..249c402e4d 100644 --- a/src/wallet/coincontrol.h +++ b/src/wallet/coincontrol.h @@ -34,6 +34,8 @@ public: boost::optional<bool> m_signal_bip125_rbf; //! 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 diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 00d2a59a66..0265433863 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -34,7 +34,7 @@ const WalletInitInterface& g_wallet_init_interface = WalletInit(); 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("-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), 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). " diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index f9baabeda4..cfa36ad40c 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -41,6 +41,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) { @@ -304,7 +315,7 @@ static UniValue setlabel(const JSONRPCRequest& request) static CTransactionRef SendMoney(interfaces::Chain::Lock& locked_chain, 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) @@ -351,7 +362,7 @@ 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( RPCHelpMan{"sendtoaddress", "\nSend an amount to a given address." + @@ -372,6 +383,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{ "\"txid\" (string) The transaction id.\n" @@ -428,6 +441,9 @@ static UniValue sendtoaddress(const JSONRPCRequest& request) } } + coin_control.m_avoid_address_reuse = GetAvoidReuseFlag(pwallet, request.params[8]); + // 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); @@ -717,7 +733,7 @@ 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( RPCHelpMan{"getbalance", "\nReturns the total available balance.\n" @@ -727,6 +743,7 @@ static UniValue getbalance(const JSONRPCRequest& request) {"dummy", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "Remains for backward compatibility. Must be excluded or set to \"*\"."}, {"minconf", RPCArg::Type::NUM, /* default */ "0", "Only include transactions confirmed at least this many times."}, {"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{ "amount (numeric) The total amount in " + CURRENCY_UNIT + " received for this wallet.\n" @@ -763,7 +780,9 @@ static UniValue getbalance(const JSONRPCRequest& request) include_watchonly = true; } - const auto bal = pwallet->GetBalance(min_depth); + bool avoid_reuse = GetAvoidReuseFlag(pwallet, request.params[3]); + + const auto bal = pwallet->GetBalance(min_depth, avoid_reuse); return ValueFromAmount(bal.m_mine_trusted + (include_watchonly ? bal.m_watchonly_trusted : 0)); } @@ -2461,6 +2480,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request) " \"paytxfee\": x.xxxx, (numeric) the transaction fee configuration, set in " + CURRENCY_UNIT + "/kB\n" " \"hdseedid\": \"<hash160>\" (string, optional) the Hash160 of the HD seed (only present when HD is enabled)\n" " \"private_keys_enabled\": true|false (boolean) false if privatekeys are disabled for this wallet (enforced watch-only wallet)\n" + " \"avoid_reuse\": true|false (boolean) whether this wallet tracks clean/dirty coins in terms of reuse\n" " \"scanning\": (json object) current scanning details, or false if no scan is in progress\n" " {\n" " \"duration\" : xxxx (numeric) elapsed seconds since scan start\n" @@ -2509,6 +2529,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request) obj.pushKV("hdseedid", seed_id.GetHex()); } obj.pushKV("private_keys_enabled", !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); + obj.pushKV("avoid_reuse", pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)); if (pwallet->IsScanning()) { UniValue scanning(UniValue::VOBJ); scanning.pushKV("duration", pwallet->ScanningDuration() / 1000); @@ -2637,6 +2658,76 @@ static UniValue loadwallet(const JSONRPCRequest& request) return obj; } +static UniValue setwalletflag(const JSONRPCRequest& request) +{ + std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request); + CWallet* const pwallet = wallet.get(); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) { + std::string flags = ""; + for (auto& it : WALLET_FLAG_MAP) + if (it.second & MUTABLE_WALLET_FLAGS) + flags += (flags == "" ? "" : ", ") + it.first; + throw std::runtime_error( + 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{ + "{\n" + " \"flag_name\": string (string) The name of the flag that was modified\n" + " \"flag_state\": bool (bool) The new state of the flag\n" + " \"warnings\": string (string) Any warnings associated with the change\n" + "}\n" + }, + RPCExamples{ + HelpExampleCli("setwalletflag", "avoid_reuse") + + HelpExampleRpc("setwalletflag", "\"avoid_reuse\"") + }, + }.ToString()); + } + + 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) { const RPCHelpMan help{ @@ -2647,6 +2738,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{ "{\n" @@ -2688,6 +2780,10 @@ static UniValue createwallet(const JSONRPCRequest& request) flags |= WALLET_FLAG_BLANK_WALLET; } + if (!request.params[4].isNull() && request.params[4].get_bool()) { + flags |= WALLET_FLAG_AVOID_REUSE; + } + WalletLocation location(request.params[0].get_str()); if (location.Exists()) { throw JSONRPCError(RPC_WALLET_ERROR, "Wallet " + location.GetName() + " already exists."); @@ -2789,6 +2885,8 @@ static UniValue listunspent(const JSONRPCRequest& request) return NullUniValue; } + bool avoid_reuse = pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); + if (request.fHelp || request.params.size() > 5) throw std::runtime_error( RPCHelpMan{"listunspent", @@ -2828,6 +2926,9 @@ static UniValue listunspent(const JSONRPCRequest& request) " \"witnessScript\" : \"script\" (string) witnessScript if the scriptPubKey is P2WSH or P2SH-P2WSH\n" " \"spendable\" : xxx, (bool) Whether we have the private keys to spend this output\n" " \"solvable\" : xxx, (bool) Whether we know how to spend this output, ignoring the lack of keys\n" + + (avoid_reuse ? + " \"reused\" : xxx, (bool) Whether this output is reused/dirty (sent to an address that was previously spent from)\n" : + "") + " \"desc\" : xxx, (string, only when solvable) A descriptor for spending this output\n" " \"safe\" : xxx (bool) Whether this output is considered safe to spend. Unconfirmed transactions\n" " from outside keys and unconfirmed replacement transactions are considered unsafe\n" @@ -2907,9 +3008,11 @@ static UniValue listunspent(const JSONRPCRequest& request) UniValue results(UniValue::VARR); std::vector<COutput> vecOutputs; { + CCoinControl cctl; + cctl.m_avoid_address_reuse = false; auto locked_chain = pwallet->chain().lock(); LOCK(pwallet->cs_wallet); - pwallet->AvailableCoins(*locked_chain, vecOutputs, !include_unsafe, nullptr, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount, nMinDepth, nMaxDepth); + pwallet->AvailableCoins(*locked_chain, vecOutputs, !include_unsafe, &cctl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount, nMinDepth, nMaxDepth); } LOCK(pwallet->cs_wallet); @@ -2918,6 +3021,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; @@ -2974,6 +3078,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); results.push_back(entry); } @@ -4185,13 +4290,13 @@ static const CRPCCommand commands[] = { "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label","address_type"} }, { "wallet", "backupwallet", &backupwallet, {"destination"} }, { "wallet", "bumpfee", &bumpfee, {"txid", "options"} }, - { "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase"} }, + { "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse"} }, { "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","include_watchonly"} }, + { "wallet", "getbalance", &getbalance, {"dummy","minconf","include_watchonly","avoid_reuse"} }, { "wallet", "getnewaddress", &getnewaddress, {"label","address_type"} }, { "wallet", "getrawchangeaddress", &getrawchangeaddress, {"address_type"} }, { "wallet", "getreceivedbyaddress", &getreceivedbyaddress, {"address","minconf"} }, @@ -4222,10 +4327,11 @@ static const CRPCCommand commands[] = { "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} }, { "wallet", "rescanblockchain", &rescanblockchain, {"start_height", "stop_height"} }, { "wallet", "sendmany", &sendmany, {"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","avoid_reuse"} }, { "wallet", "sethdseed", &sethdseed, {"newkeypool","seed"} }, { "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"} }, diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index bde52d7791..0472334bc1 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -36,6 +36,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; @@ -932,6 +940,37 @@ bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash) return success; } +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); @@ -940,6 +979,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; @@ -1557,7 +1604,7 @@ void CWallet::UnsetWalletFlagWithDB(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); } @@ -1566,7 +1613,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; } @@ -2059,7 +2106,7 @@ CAmount CWalletTx::GetAvailableCredit(interfaces::Chain::Lock& locked_chain, boo 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(locked_chain)) @@ -2069,12 +2116,12 @@ CAmount CWalletTx::GetAvailableCredit(interfaces::Chain::Lock& locked_chain, boo 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(locked_chain, hashTx, i)) - { + if (!pwallet->IsSpent(locked_chain, hashTx, i) && (allow_used_addresses || !pwallet->IsUsedDestination(hashTx, i))) { const CTxOut &txout = tx->vout[i]; nCredit += pwallet->GetCredit(txout, filter); if (!MoneyRange(nCredit)) @@ -2216,9 +2263,10 @@ void MaybeResendWalletTxs() */ -CWallet::Balance CWallet::GetBalance(const int min_depth) const +CWallet::Balance CWallet::GetBalance(const int min_depth, bool avoid_reuse) const { Balance ret; + isminefilter reuse_filter = avoid_reuse ? 0 : ISMINE_USED; { auto locked_chain = chain().lock(); LOCK(cs_wallet); @@ -2227,8 +2275,8 @@ CWallet::Balance CWallet::GetBalance(const int min_depth) const const CWalletTx& wtx = entry.second; const bool is_trusted{wtx.IsTrusted(*locked_chain)}; const int tx_depth{wtx.GetDepthInMainChain(*locked_chain)}; - const CAmount tx_credit_mine{wtx.GetAvailableCredit(*locked_chain, /* fUseCache */ true, ISMINE_SPENDABLE)}; - const CAmount tx_credit_watchonly{wtx.GetAvailableCredit(*locked_chain, /* fUseCache */ true, ISMINE_WATCH_ONLY)}; + const CAmount tx_credit_mine{wtx.GetAvailableCredit(*locked_chain, /* fUseCache */ true, ISMINE_SPENDABLE | reuse_filter)}; + const CAmount tx_credit_watchonly{wtx.GetAvailableCredit(*locked_chain, /* fUseCache */ true, ISMINE_WATCH_ONLY | reuse_filter)}; if (is_trusted && tx_depth >= min_depth) { ret.m_mine_trusted += tx_credit_mine; ret.m_watchonly_trusted += tx_credit_watchonly; @@ -2266,6 +2314,9 @@ void CWallet::AvailableCoins(interfaces::Chain::Lock& locked_chain, std::vector< vCoins.clear(); 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 (const auto& entry : mapWallet) { @@ -2347,6 +2398,10 @@ void CWallet::AvailableCoins(interfaces::Chain::Lock& locked_chain, std::vector< continue; } + if (!allow_used_addresses && IsUsedDestination(wtxid, i)) { + continue; + } + bool solvable = IsSolvable(*this, wtx.tx->vout[i].scriptPubKey); bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable)); @@ -4137,16 +4192,12 @@ std::shared_ptr<CWallet> CWallet::CreateWalletFromFile(interfaces::Chain& chain, // ensure this wallet.dat can only be opened by clients supporting HD with chain split and expects no default key walletInstance->SetMinVersion(FEATURE_LATEST); - 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))) { // generate a new seed CPubKey seed = walletInstance->GenerateNewSeed(); walletInstance->SetHDSeed(seed); - } // Otherwise, do not generate a new seed + } // Top up the keypool if (walletInstance->CanGenerateKeys() && !walletInstance->TopUpKeyPool()) { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 87aff09039..c2d1102c33 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -120,6 +120,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), @@ -139,7 +143,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 * @@ -924,6 +944,12 @@ public: std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params, bool& bnb_used) const; bool IsSpent(interfaces::Chain::Lock& locked_chain, 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); @@ -1036,7 +1062,7 @@ public: CAmount m_watchonly_untrusted_pending{0}; CAmount m_watchonly_immature{0}; }; - Balance GetBalance(int min_depth = 0) const; + Balance GetBalance(int min_depth = 0, bool avoid_reuse = true) const; CAmount GetAvailableBalance(const CCoinControl* coinControl = nullptr) const; OutputType TransactionChangeType(OutputType change_type, const std::vector<CRecipient>& vecSend); @@ -1288,7 +1314,7 @@ public: void UnsetWalletFlag(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 */ diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 80dced733e..098dbd647b 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -120,6 +120,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', diff --git a/test/functional/wallet_avoidreuse.py b/test/functional/wallet_avoidreuse.py new file mode 100755 index 0000000000..1dec040f68 --- /dev/null +++ b/test/functional/wallet_avoidreuse.py @@ -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_bi, +) + +# 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_bi(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() |