diff --git a/src/privatesend/privatesend-client.cpp b/src/privatesend/privatesend-client.cpp index 775db8b1bc9a..e0aac8c470a7 100644 --- a/src/privatesend/privatesend-client.cpp +++ b/src/privatesend/privatesend-client.cpp @@ -1254,7 +1254,7 @@ bool CPrivateSendClientSession::SubmitDenominate(CConnman& connman) std::vector > vecInputsByRounds; - for (int i = 0; i < CPrivateSendClientOptions::GetRounds(); i++) { + for (int i = 0; i < CPrivateSendClientOptions::GetRounds() + CPrivateSendClientOptions::GetRandomRounds(); i++) { if (PrepareDenominate(i, i, strError, vecPSInOutPairs, vecPSInOutPairsTmp, true)) { LogPrint(BCLog::PRIVATESEND, "CPrivateSendClientSession::SubmitDenominate -- Running PrivateSend denominate for %d rounds, success\n", i); vecInputsByRounds.emplace_back(i, vecPSInOutPairsTmp.size()); diff --git a/src/privatesend/privatesend-client.h b/src/privatesend/privatesend-client.h index 6b979f94a745..74ac2d95b956 100644 --- a/src/privatesend/privatesend-client.h +++ b/src/privatesend/privatesend-client.h @@ -54,6 +54,8 @@ static const int PRIVATESEND_DENOM_OUTPUTS_THRESHOLD = 500; static const int PRIVATESEND_KEYS_THRESHOLD_WARNING = 100; // Stop mixing completely, it's too dangerous to continue when we have only this many keys left static const int PRIVATESEND_KEYS_THRESHOLD_STOP = 50; +// Pseudorandomly mix up to this many times in addition to base round count +static const int PRIVATESEND_RANDOM_ROUNDS = 3; // The main object for accessing mixing extern std::map privateSendClientManagers; @@ -224,8 +226,8 @@ class CPrivateSendClientManager bool CheckAutomaticBackup(); public: - int nCachedNumBlocks; //used for the overview screen - bool fCreateAutoBackups; //builtin support for automatic backups + int nCachedNumBlocks; // used for the overview screen + bool fCreateAutoBackups; // builtin support for automatic backups CPrivateSendClientManager() : vecMasternodesUsed(), @@ -280,6 +282,7 @@ class CPrivateSendClientOptions public: static int GetSessions() { return CPrivateSendClientOptions::Get().nPrivateSendSessions; } static int GetRounds() { return CPrivateSendClientOptions::Get().nPrivateSendRounds; } + static int GetRandomRounds() { return CPrivateSendClientOptions::Get().nPrivateSendRandomRounds; } static int GetAmount() { return CPrivateSendClientOptions::Get().nPrivateSendAmount; } static int GetDenomsGoal() { return CPrivateSendClientOptions::Get().nPrivateSendDenomsGoal; } static int GetDenomsHardCap() { return CPrivateSendClientOptions::Get().nPrivateSendDenomsHardCap; } @@ -301,6 +304,7 @@ class CPrivateSendClientOptions CCriticalSection cs_ps_options; int nPrivateSendSessions; int nPrivateSendRounds; + int nPrivateSendRandomRounds; int nPrivateSendAmount; int nPrivateSendDenomsGoal; int nPrivateSendDenomsHardCap; @@ -309,6 +313,7 @@ class CPrivateSendClientOptions CPrivateSendClientOptions() : nPrivateSendRounds(DEFAULT_PRIVATESEND_ROUNDS), + nPrivateSendRandomRounds(PRIVATESEND_RANDOM_ROUNDS), nPrivateSendAmount(DEFAULT_PRIVATESEND_AMOUNT), nPrivateSendDenomsGoal(DEFAULT_PRIVATESEND_DENOMS_GOAL), nPrivateSendDenomsHardCap(DEFAULT_PRIVATESEND_DENOMS_HARDCAP), diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index 9f1dee92ac74..b4bb19efe97e 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -706,9 +706,8 @@ void CoinControlDialog::updateView() int nChildren = 0; for (const COutput& out : coins.second) { COutPoint outpoint = COutPoint(out.tx->tx->GetHash(), out.i); - int nRounds = model->getRealOutpointPrivateSendRounds(outpoint); - if ((coinControl()->IsUsingPrivateSend() && nRounds >= CPrivateSendClientOptions::GetRounds()) || !(coinControl()->IsUsingPrivateSend())) { + if ((coinControl()->IsUsingPrivateSend() && model->isFullyMixed(outpoint)) || !(coinControl()->IsUsingPrivateSend())) { nSum += out.tx->tx->vout[out.i].nValue; nChildren++; @@ -759,6 +758,7 @@ void CoinControlDialog::updateView() itemOutput->setData(COLUMN_DATE, Qt::UserRole, QVariant((qlonglong)out.tx->GetTxTime())); // PrivateSend rounds + int nRounds = model->getRealOutpointPrivateSendRounds(outpoint); if (nRounds >= 0 || LogAcceptCategory(BCLog::PRIVATESEND)) { itemOutput->setText(COLUMN_PRIVATESEND_ROUNDS, QString::number(nRounds)); } else { diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index b2f330dcf8f9..728cb239a7da 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -229,6 +229,11 @@ int WalletModel::getRealOutpointPrivateSendRounds(const COutPoint& outpoint) con return wallet->GetRealOutpointPrivateSendRounds(outpoint); } +bool WalletModel::isFullyMixed(const COutPoint& outpoint) const +{ + return wallet->IsFullyMixed(outpoint); +} + void WalletModel::updateAddressBook(const QString &address, const QString &label, bool isMine, const QString &purpose, int status) { diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index f18814d20e70..28cbc6c0bf56 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -232,6 +232,7 @@ class WalletModel : public QObject int getNumISLocks() const; int getRealOutpointPrivateSendRounds(const COutPoint& outpoint) const; + bool isFullyMixed(const COutPoint& outpoint) const; QString getWalletName() const; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index db4e100dd9e7..89be10b19857 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3167,7 +3167,7 @@ UniValue listunspent(const JSONRPCRequest& request) entry.pushKV("spendable", out.fSpendable); entry.pushKV("solvable", out.fSolvable); entry.pushKV("safe", out.fSafe); - entry.pushKV("ps_rounds", pwallet->GetCappedOutpointPrivateSendRounds(COutPoint(out.tx->GetHash(), out.i))); + entry.pushKV("ps_rounds", pwallet->GetRealOutpointPrivateSendRounds(COutPoint(out.tx->GetHash(), out.i))); results.push_back(entry); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 81ba4a4658a5..4ee537219fbf 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1583,9 +1583,11 @@ int CWallet::GetRealOutpointPrivateSendRounds(const COutPoint& outpoint, int nRo { LOCK(cs_wallet); - if (nRounds >= MAX_PRIVATESEND_ROUNDS) { - // there can only be MAX_PRIVATESEND_ROUNDS rounds max - return MAX_PRIVATESEND_ROUNDS - 1; + const int nRoundsMax = MAX_PRIVATESEND_ROUNDS + CPrivateSendClientOptions::GetRandomRounds(); + + if (nRounds >= nRoundsMax) { + // there can only be nRoundsMax rounds max + return nRoundsMax - 1; } auto pair = mapOutpointRoundsCache.emplace(outpoint, -10); @@ -1651,7 +1653,7 @@ int CWallet::GetRealOutpointPrivateSendRounds(const COutPoint& outpoint, int nRo } } *nRoundsRef = fDenomFound - ? (nShortest >= MAX_PRIVATESEND_ROUNDS - 1 ? MAX_PRIVATESEND_ROUNDS : nShortest + 1) // good, we a +1 to the shortest one but only MAX_PRIVATESEND_ROUNDS rounds max allowed + ? (nShortest >= nRoundsMax - 1 ? nRoundsMax : nShortest + 1) // good, we a +1 to the shortest one but only nRoundsMax rounds max allowed : 0; // too bad, we are the fist one in that chain LogPrint(BCLog::PRIVATESEND, "%s UPDATED %-70s %3d\n", __func__, outpoint.ToStringShort(), *nRoundsRef); return *nRoundsRef; @@ -1681,6 +1683,29 @@ bool CWallet::IsDenominated(const COutPoint& outpoint) const return CPrivateSend::IsDenominatedAmount(it->second.tx->vout[outpoint.n].nValue); } +bool CWallet::IsFullyMixed(const COutPoint& outpoint) const +{ + int nRounds = GetRealOutpointPrivateSendRounds(outpoint); + // Mix again if we don't have N rounds yet + if (nRounds < CPrivateSendClientOptions::GetRounds()) return false; + + // Try to mix a "random" number of rounds more than minimum. + // If we have already mixed N + MaxOffset rounds, don't mix again. + // Otherwise, we should mix again 50% of the time, this results in an exponential decay + // N rounds 50% N+1 25% N+2 12.5%... until we reach N + GetRandomRounds() rounds where we stop. + if (nRounds < CPrivateSendClientOptions::GetRounds() + CPrivateSendClientOptions::GetRandomRounds()) { + CDataStream ss(SER_GETHASH, PROTOCOL_VERSION); + ss << outpoint << nPrivateSendSalt; + uint256 nHash; + CSHA256().Write((const unsigned char*)ss.data(), ss.size()).Finalize(nHash.begin()); + if (nHash.GetCheapHash() % 2 == 0) { + return false; + } + } + + return true; +} + isminetype CWallet::IsMine(const CTxOut& txout) const { return ::IsMine(*this, txout.scriptPubKey); @@ -2363,8 +2388,7 @@ CAmount CWalletTx::GetAnonymizedCredit(bool fUseCache) const if (pwallet->IsSpent(hashTx, i) || !CPrivateSend::IsDenominatedAmount(txout.nValue)) continue; - const int nRounds = pwallet->GetCappedOutpointPrivateSendRounds(outpoint); - if (nRounds >= CPrivateSendClientOptions::GetRounds()) { + if (pwallet->IsFullyMixed(outpoint)) { nCredit += pwallet->GetCredit(txout, ISMINE_SPENDABLE); if (!MoneyRange(nCredit)) throw std::runtime_error(std::string(__func__) + ": value out of range"); @@ -2830,12 +2854,10 @@ void CWallet::AvailableCoins(std::vector &vCoins, bool fOnlySafe, const bool found = false; if (nCoinType == CoinType::ONLY_FULLY_MIXED) { if (!CPrivateSend::IsDenominatedAmount(pcoin->tx->vout[i].nValue)) continue; - int nRounds = GetCappedOutpointPrivateSendRounds(COutPoint(wtxid, i)); - found = nRounds >= CPrivateSendClientOptions::GetRounds(); + found = IsFullyMixed(COutPoint(wtxid, i)); } else if(nCoinType == CoinType::ONLY_READY_TO_MIX) { if (!CPrivateSend::IsDenominatedAmount(pcoin->tx->vout[i].nValue)) continue; - int nRounds = GetCappedOutpointPrivateSendRounds(COutPoint(wtxid, i)); - found = nRounds < CPrivateSendClientOptions::GetRounds(); + found = !IsFullyMixed(COutPoint(wtxid, i)); } else if(nCoinType == CoinType::ONLY_NONDENOMINATED) { if (CPrivateSend::IsCollateralAmount(pcoin->tx->vout[i].nValue)) continue; // do not use collateral amounts found = !CPrivateSend::IsDenominatedAmount(pcoin->tx->vout[i].nValue); @@ -2951,6 +2973,21 @@ const CTxOut& CWallet::FindNonChangeParentOutput(const CTransaction& tx, int out return ptx->vout[n]; } +void CWallet::InitPrivateSendSalt() +{ + // Avoid fetching it multiple times + assert(nPrivateSendSalt.IsNull()); + + WalletBatch batch(*database); + batch.ReadPrivateSendSalt(nPrivateSendSalt); + + while (nPrivateSendSalt.IsNull()) { + // We never generated/saved it + nPrivateSendSalt = GetRandHash(); + batch.WritePrivateSendSalt(nPrivateSendSalt); + } +} + static void ApproximateBestSubset(const std::vector& vValue, const CAmount& nTotalLower, const CAmount& nTargetValue, std::vector& vfBest, CAmount& nBest, int iterations = 1000) { @@ -3216,8 +3253,7 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm if (nCoinType == CoinType::ONLY_FULLY_MIXED) { // Make sure to include mixed preset inputs only, // even if some non-mixed inputs were manually selected via CoinControl - int nRounds = GetRealOutpointPrivateSendRounds(outpoint); - if (nRounds < CPrivateSendClientOptions::GetRounds()) continue; + if (!IsFullyMixed(outpoint)) continue; } nValueFromPresetInputs += pcoin->tx->vout[outpoint.n].nValue; setPresetCoins.insert(CInputCoin(pcoin, outpoint.n)); @@ -3420,7 +3456,7 @@ bool CWallet::SelectCoinsGroupedByAddresses(std::vector& vecTa // otherwise they will just lead to higher fee / lower priority if(wtx.tx->vout[i].nValue <= nSmallestDenom/10) continue; // ignore mixed - if (GetCappedOutpointPrivateSendRounds(COutPoint(outpoint.hash, i)) >= CPrivateSendClientOptions::GetRounds()) continue; + if (IsFullyMixed(COutPoint(outpoint.hash, i))) continue; } if (itTallyItem == mapTally.end()) { @@ -4194,6 +4230,8 @@ DBErrors CWallet::LoadWallet(bool& fFirstRunRet) } } + InitPrivateSendSalt(); + if (nLoadWalletRet != DBErrors::LOAD_OK) return nLoadWalletRet; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index d665c36872e4..c94ff9b771f5 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -802,6 +802,17 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface */ const CBlockIndex* m_last_block_processed = nullptr; + /** Pulled from wallet DB ("ps_salt") and used when mixing a random number of rounds. + * This salt is needed to prevent an attacker from learning how many extra times + * the input was mixed based only on information in the blockchain. + */ + uint256 nPrivateSendSalt; + + /** + * Fetches PrivateSend salt from database or generates and saves a new one if no salt was found in the db + */ + void InitPrivateSendSalt(); + public: /* * Main wallet lock. @@ -912,6 +923,7 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface int GetCappedOutpointPrivateSendRounds(const COutPoint& outpoint) const; bool IsDenominated(const COutPoint& outpoint) const; + bool IsFullyMixed(const COutPoint& outpoint) const; bool IsSpent(const uint256& hash, unsigned int n) const; diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 61c751b9b228..2bc01ac01c98 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -168,6 +168,16 @@ bool WalletBatch::WriteAccountingEntry(const uint64_t nAccEntryNum, const CAccou return WriteIC(std::make_pair(std::string("acentry"), std::make_pair(acentry.strAccount, nAccEntryNum)), acentry); } +bool WalletBatch::ReadPrivateSendSalt(uint256& salt) +{ + return m_batch.Read(std::string("ps_salt"), salt); +} + +bool WalletBatch::WritePrivateSendSalt(const uint256& salt) +{ + return WriteIC(std::string("ps_salt"), salt); +} + CAmount WalletBatch::GetAccountCreditDebit(const std::string& strAccount) { std::list entries; diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index d6a2a9e92a80..3477c1eb1b68 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -160,6 +160,9 @@ class WalletBatch bool ReadAccount(const std::string& strAccount, CAccount& account); bool WriteAccount(const std::string& strAccount, const CAccount& account); + bool ReadPrivateSendSalt(uint256& salt); + bool WritePrivateSendSalt(const uint256& salt); + /// Write destination data key,value tuple to database bool WriteDestData(const std::string &address, const std::string &key, const std::string &value); /// Erase destination data tuple from wallet database