From 5943c93bdd36a7c4ee96e94d60772814111f7cd8 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sat, 20 Jul 2024 05:09:25 +0000 Subject: [PATCH] feat: introduce `coinjoinsalt` RPC to allow manipulating per-wallet salt Co-authored-by: thephez Co-authored-by: UdjinM6 --- doc/release-notes-6093.md | 4 + src/rpc/coinjoin.cpp | 191 ++++++++++++++++++++++++++++++++++++++ src/wallet/wallet.cpp | 9 ++ src/wallet/wallet.h | 4 +- 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 doc/release-notes-6093.md diff --git a/doc/release-notes-6093.md b/doc/release-notes-6093.md new file mode 100644 index 0000000000..a61b40c9a6 --- /dev/null +++ b/doc/release-notes-6093.md @@ -0,0 +1,4 @@ +New functionality +----------- + +- A new RPC command, `coinjoinsalt`, allows for manipulating a CoinJoin salt stored in a wallet. `coinjoinsalt get` will fetch an existing salt, `coinjoinsalt set` will allow setting a custom salt and `coinjoinsalt generate` will set a random hash as the new salt. diff --git a/src/rpc/coinjoin.cpp b/src/rpc/coinjoin.cpp index bbca4ba11c..68210881a7 100644 --- a/src/rpc/coinjoin.cpp +++ b/src/rpc/coinjoin.cpp @@ -181,6 +181,193 @@ static RPCHelpMan coinjoin_stop() }, }; } + +static RPCHelpMan coinjoinsalt() +{ + return RPCHelpMan{"coinjoinsalt", + "\nAvailable commands:\n" + " generate - Generate new CoinJoin salt\n" + " get - Fetch existing CoinJoin salt\n" + " set - Set new CoinJoin salt\n", + { + {"command", RPCArg::Type::STR, RPCArg::Optional::NO, "The command to execute"}, + }, + RPCResults{}, + RPCExamples{""}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + throw JSONRPCError(RPC_INVALID_PARAMETER, "Must be a valid command"); +}, + }; +} + +static RPCHelpMan coinjoinsalt_generate() +{ + return RPCHelpMan{"coinjoinsalt generate", + "\nGenerate new CoinJoin salt and store it in the wallet database\n" + "Cannot generate new salt if CoinJoin mixing is in process or wallet has private keys disabled.\n", + { + {"overwrite", RPCArg::Type::BOOL, /* default */ "false", "Generate new salt even if there is an existing salt and/or there is CoinJoin balance"}, + }, + RPCResult{ + RPCResult::Type::BOOL, "", "Status of CoinJoin salt generation and commitment" + }, + RPCExamples{ + HelpExampleCli("coinjoinsalt generate", "") + + HelpExampleRpc("coinjoinsalt generate", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + + const auto str_wallet = wallet->GetName(); + if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_INVALID_REQUEST, + strprintf("Wallet \"%s\" has private keys disabled, cannot perform CoinJoin!", str_wallet)); + } + + bool enable_overwrite{false}; // Default value + if (!request.params[0].isNull()) { + enable_overwrite = ParseBoolV(request.params[0], "overwrite"); + } + + if (!enable_overwrite && !wallet->GetCoinJoinSalt().IsNull()) { + throw JSONRPCError(RPC_INVALID_REQUEST, + strprintf("Wallet \"%s\" already has set CoinJoin salt!", str_wallet)); + } + + const NodeContext& node = EnsureAnyNodeContext(request.context); + if (node.coinjoin_loader != nullptr) { + auto cj_clientman = node.coinjoin_loader->walletman().Get(wallet->GetName()); + if (cj_clientman != nullptr && cj_clientman->IsMixing()) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Wallet \"%s\" is currently mixing, cannot change salt!", str_wallet)); + } + } + + const auto wallet_balance{wallet->GetBalance()}; + const bool has_balance{(wallet_balance.m_anonymized + + wallet_balance.m_denominated_trusted + + wallet_balance.m_denominated_untrusted_pending) > 0}; + if (!enable_overwrite && has_balance) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Wallet \"%s\" has CoinJoin balance, cannot continue!", str_wallet)); + } + + if (!wallet->SetCoinJoinSalt(GetRandHash())) { + throw JSONRPCError(RPC_INVALID_REQUEST, + strprintf("Unable to set new CoinJoin salt for wallet \"%s\"!", str_wallet)); + } + + wallet->ClearCoinJoinRoundsCache(); + + return true; +}, + }; +} + +static RPCHelpMan coinjoinsalt_get() +{ + return RPCHelpMan{"coinjoinsalt get", + "\nFetch existing CoinJoin salt\n" + "Cannot fetch salt if wallet has private keys disabled.\n", + {}, + RPCResult{ + RPCResult::Type::STR_HEX, "", "CoinJoin salt" + }, + RPCExamples{ + HelpExampleCli("coinjoinsalt get", "") + + HelpExampleRpc("coinjoinsalt get", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + + const auto str_wallet = wallet->GetName(); + if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_INVALID_REQUEST, + strprintf("Wallet \"%s\" has private keys disabled, cannot perform CoinJoin!", str_wallet)); + } + + const auto salt{wallet->GetCoinJoinSalt()}; + if (salt.IsNull()) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Wallet \"%s\" has no CoinJoin salt!", str_wallet)); + } + return salt.GetHex(); +}, + }; +} + +static RPCHelpMan coinjoinsalt_set() +{ + return RPCHelpMan{"coinjoinsalt set", + "\nSet new CoinJoin salt\n" + "Cannot set salt if CoinJoin mixing is in process or wallet has private keys disabled.\n" + "Will overwrite existing salt. The presence of a CoinJoin balance will cause the wallet to rescan.\n", + { + {"salt", RPCArg::Type::STR, RPCArg::Optional::NO, "Desired CoinJoin salt value for the wallet"}, + {"overwrite", RPCArg::Type::BOOL, /* default */ "false", "Overwrite salt even if CoinJoin balance present"}, + }, + RPCResult{ + RPCResult::Type::BOOL, "", "Status of CoinJoin salt change request" + }, + RPCExamples{ + HelpExampleCli("coinjoinsalt set", "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16") + + HelpExampleRpc("coinjoinsalt set", "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + + const auto salt{ParseHashV(request.params[0], "salt")}; + if (salt == uint256::ZERO) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid CoinJoin salt value"); + } + + bool enable_overwrite{false}; // Default value + if (!request.params[1].isNull()) { + enable_overwrite = ParseBoolV(request.params[1], "overwrite"); + } + + const auto str_wallet = wallet->GetName(); + if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_INVALID_REQUEST, + strprintf("Wallet \"%s\" has private keys disabled, cannot perform CoinJoin!", str_wallet)); + } + + const NodeContext& node = EnsureAnyNodeContext(request.context); + if (node.coinjoin_loader != nullptr) { + auto cj_clientman = node.coinjoin_loader->walletman().Get(wallet->GetName()); + if (cj_clientman != nullptr && cj_clientman->IsMixing()) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Wallet \"%s\" is currently mixing, cannot change salt!", str_wallet)); + } + } + + const auto wallet_balance{wallet->GetBalance()}; + const bool has_balance{(wallet_balance.m_anonymized + + wallet_balance.m_denominated_trusted + + wallet_balance.m_denominated_untrusted_pending) > 0}; + if (has_balance && !enable_overwrite) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Wallet \"%s\" has CoinJoin balance, cannot continue!", str_wallet)); + } + + if (!wallet->SetCoinJoinSalt(salt)) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Unable to set new CoinJoin salt for wallet \"%s\"!", str_wallet)); + } + + wallet->ClearCoinJoinRoundsCache(); + + return true; +}, + }; +} #endif // ENABLE_WALLET static RPCHelpMan getpoolinfo() @@ -295,6 +482,10 @@ static const CRPCCommand commands[] = { "dash", &coinjoin_reset, }, { "dash", &coinjoin_start, }, { "dash", &coinjoin_stop, }, + { "dash", &coinjoinsalt, }, + { "dash", &coinjoinsalt_generate, }, + { "dash", &coinjoinsalt_get, }, + { "dash", &coinjoinsalt_set, }, #endif // ENABLE_WALLET }; // clang-format on diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 53702f6c52..f97c0ac9e8 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1448,6 +1448,15 @@ int CWallet::GetCappedOutpointCoinJoinRounds(const COutPoint& outpoint) const return realCoinJoinRounds > CCoinJoinClientOptions::GetRounds() ? CCoinJoinClientOptions::GetRounds() : realCoinJoinRounds; } +void CWallet::ClearCoinJoinRoundsCache() +{ + LOCK(cs_wallet); + mapOutpointRoundsCache.clear(); + MarkDirty(); + // Notify UI + NotifyTransactionChanged(uint256::ONE, CT_UPDATED); +} + bool CWallet::IsDenominated(const COutPoint& outpoint) const { LOCK(cs_wallet); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index d73af772df..217cfbaad0 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -747,7 +747,7 @@ private: void AddToSpends(const uint256& wtxid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); std::set setWalletUTXO; - mutable std::map mapOutpointRoundsCache; + mutable std::map mapOutpointRoundsCache GUARDED_BY(cs_wallet); /** * Add a transaction to the wallet, or update it. pIndex and posInBlock should @@ -995,6 +995,8 @@ public: int GetRealOutpointCoinJoinRounds(const COutPoint& outpoint, int nRounds = 0) const; // respect current settings int GetCappedOutpointCoinJoinRounds(const COutPoint& outpoint) const; + // drop the internal cache to let Get...Rounds recalculate CJ balance from scratch and notify UI + void ClearCoinJoinRoundsCache(); bool IsDenominated(const COutPoint& outpoint) const; bool IsFullyMixed(const COutPoint& outpoint) const;