Skip to content

Commit

Permalink
feat: introduce coinjoinsalt RPC to allow manipulating per-wallet salt
Browse files Browse the repository at this point in the history
Co-authored-by: UdjinM6 <[email protected]>
  • Loading branch information
kwvg and UdjinM6 committed Jul 11, 2024
1 parent 54c99fd commit 019a3c0
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 8 deletions.
4 changes: 4 additions & 0 deletions doc/release-notes-6093.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "getspecialtxes", 4, "verbosity" },
{ "disconnectnode", 1, "nodeid" },
{ "upgradewallet", 0, "version" },
// Composite commands
{ "coinjoinsalt", "generate", 0, "overwrite" },
{ "coinjoinsalt", "set", 1, "overwrite" },
// Echo with conversion (For testing only)
{ "echojson", 0, "arg0" },
{ "echojson", 1, "arg1" },
Expand Down
198 changes: 192 additions & 6 deletions src/rpc/coinjoin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,188 @@ static RPCHelpMan coinjoin()
},
};
}

// From wallet/rpcdump.cpp
extern void RescanWallet(CWallet& wallet, const WalletRescanReserver& reserver, int64_t time_begin = 0, bool update = true);

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 commit to 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<CWallet> 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 bool enable_overwrite{!request.params[0].isNull() ? request.params[0].get_bool() : /* default */ false};
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<CWallet> 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<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;

const auto salt{ParseHashV(request.params[0], "salt")};
const bool enable_overwrite{!request.params[1].isNull() ? request.params[1].get_bool() : /* default */ false};
if (salt == uint256::ZERO) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Illegal CoinJoin salt value");
}

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_INVALID_REQUEST,
strprintf("Unable to set new CoinJoin salt for wallet \"%s\"!", str_wallet));
}

wallet->ClearCoinJoinRoundsCache();

return true;
},
};
}
#endif // ENABLE_WALLET

static RPCHelpMan getpoolinfo()
Expand Down Expand Up @@ -191,16 +373,20 @@ void RegisterCoinJoinRPCCommands(CRPCTable &t)
{
// clang-format off
static const CRPCCommand commands[] =
{ // category name actor (function) argNames
// --------------------- ------------------------ ---------------------------------
{ "dash", "getpoolinfo", &getpoolinfo, {} },
{ "dash", "getcoinjoininfo", &getcoinjoininfo, {} },
{ // category name actor (function) argNames
// ------------------------------------------------------------------------------------------------------
{ "dash", "getpoolinfo", &getpoolinfo, {} },
{ "dash", "getcoinjoininfo", &getcoinjoininfo, {} },
#ifdef ENABLE_WALLET
{ "dash", "coinjoin", &coinjoin, {"command"} },
{ "dash", "coinjoin", &coinjoin, {"command"} },
{ "dash", "coinjoinsalt", &coinjoinsalt, {"command"} },
{ "dash", "coinjoinsalt", "generate", &coinjoinsalt_generate, {"overwrite"} },
{ "dash", "coinjoinsalt", "get", &coinjoinsalt_get, {} },
{ "dash", "coinjoinsalt", "set", &coinjoinsalt_set, {"salt", "overwrite"} },
#endif // ENABLE_WALLET
};
// clang-format on
for (const auto& command : commands) {
t.appendCommand(command.name, &command);
t.appendCommand(command.name, command.subname, &command);
}
}
2 changes: 1 addition & 1 deletion src/wallet/rpcdump.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ static std::string DecodeDumpString(const std::string &str) {

static const int64_t TIMESTAMP_MIN = 0;

static void RescanWallet(CWallet& wallet, const WalletRescanReserver& reserver, int64_t time_begin = TIMESTAMP_MIN, bool update = true)
void RescanWallet(CWallet& wallet, const WalletRescanReserver& reserver, int64_t time_begin = TIMESTAMP_MIN, bool update = true)
{
int64_t scanned_time = wallet.RescanFromTime(time_begin, reserver, update);
if (wallet.IsAbortingRescan()) {
Expand Down
9 changes: 9 additions & 0 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion src/wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
void AddToSpends(const uint256& wtxid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

std::set<COutPoint> setWalletUTXO;
mutable std::map<COutPoint, int> mapOutpointRoundsCache;
mutable std::map<COutPoint, int> mapOutpointRoundsCache GUARDED_BY(cs_wallet);

/**
* Add a transaction to the wallet, or update it. pIndex and posInBlock should
Expand Down Expand Up @@ -995,6 +995,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
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;
Expand Down

0 comments on commit 019a3c0

Please sign in to comment.