Skip to content

Commit 9ded950

Browse files
authored
Merge pull request #3702 from PastaPastaPasta/backport-v16-3661
[v0.16.x] Backport #3661
2 parents 5c28615 + 08f2437 commit 9ded950

10 files changed

Lines changed: 92 additions & 19 deletions

File tree

src/privatesend/privatesend-client.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1199,7 +1199,7 @@ bool CPrivateSendClientSession::SubmitDenominate(CConnman& connman)
11991199

12001200
std::vector<std::pair<int, size_t> > vecInputsByRounds;
12011201

1202-
for (int i = 0; i < privateSendClient.nPrivateSendRounds; i++) {
1202+
for (int i = 0; i < privateSendClient.nPrivateSendRounds + privateSendClient.nPrivateSendRandomRounds; i++) {
12031203
if (PrepareDenominate(i, i, strError, vecPSInOutPairs, vecPSInOutPairsTmp, true)) {
12041204
LogPrint(BCLog::PRIVATESEND, "CPrivateSendClientSession::SubmitDenominate -- Running PrivateSend denominate for %d rounds, success\n", i);
12051205
vecInputsByRounds.emplace_back(i, vecPSInOutPairsTmp.size());

src/privatesend/privatesend-client.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ static const int PRIVATESEND_DENOM_OUTPUTS_THRESHOLD = 500;
5050
static const int PRIVATESEND_KEYS_THRESHOLD_WARNING = 100;
5151
// Stop mixing completely, it's too dangerous to continue when we have only this many keys left
5252
static const int PRIVATESEND_KEYS_THRESHOLD_STOP = 50;
53+
// Pseudorandomly mix up to this many times in addition to base round count
54+
static const int PRIVATESEND_RANDOM_ROUNDS = 3;
5355

5456
// The main object for accessing mixing
5557
extern CPrivateSendClientManager privateSendClient;
@@ -204,15 +206,16 @@ class CPrivateSendClientManager : public CPrivateSendBaseManager
204206
public:
205207
int nPrivateSendSessions;
206208
int nPrivateSendRounds;
209+
int nPrivateSendRandomRounds;
207210
int nPrivateSendAmount;
208211
int nPrivateSendDenomsGoal;
209212
int nPrivateSendDenomsHardCap;
210213
bool fEnablePrivateSend;
211214
bool fPrivateSendRunning;
212215
bool fPrivateSendMultiSession;
213216

214-
int nCachedNumBlocks; //used for the overview screen
215-
bool fCreateAutoBackups; //builtin support for automatic backups
217+
int nCachedNumBlocks; // used for the overview screen
218+
bool fCreateAutoBackups; // builtin support for automatic backups
216219

217220
CPrivateSendClientManager() :
218221
vecMasternodesUsed(),
@@ -222,6 +225,7 @@ class CPrivateSendClientManager : public CPrivateSendBaseManager
222225
strAutoDenomResult(),
223226
nCachedBlockHeight(0),
224227
nPrivateSendRounds(DEFAULT_PRIVATESEND_ROUNDS),
228+
nPrivateSendRandomRounds(PRIVATESEND_RANDOM_ROUNDS),
225229
nPrivateSendAmount(DEFAULT_PRIVATESEND_AMOUNT),
226230
nPrivateSendDenomsGoal(DEFAULT_PRIVATESEND_DENOMS_GOAL),
227231
nPrivateSendDenomsHardCap(DEFAULT_PRIVATESEND_DENOMS_HARDCAP),

src/qt/coincontroldialog.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -724,9 +724,8 @@ void CoinControlDialog::updateView()
724724
int nChildren = 0;
725725
for (const COutput& out : coins.second) {
726726
COutPoint outpoint = COutPoint(out.tx->tx->GetHash(), out.i);
727-
int nRounds = model->getRealOutpointPrivateSendRounds(outpoint);
728727

729-
if ((coinControl()->IsUsingPrivateSend() && nRounds >= privateSendClient.nPrivateSendRounds) || !(coinControl()->IsUsingPrivateSend())) {
728+
if ((coinControl()->IsUsingPrivateSend() && model->isFullyMixed(outpoint)) || !(coinControl()->IsUsingPrivateSend())) {
730729
nSum += out.tx->tx->vout[out.i].nValue;
731730
nChildren++;
732731

@@ -777,6 +776,7 @@ void CoinControlDialog::updateView()
777776
itemOutput->setData(COLUMN_DATE, Qt::UserRole, QVariant((qlonglong)out.tx->GetTxTime()));
778777

779778
// PrivateSend rounds
779+
int nRounds = model->getRealOutpointPrivateSendRounds(outpoint);
780780
if (nRounds >= 0 || LogAcceptCategory(BCLog::PRIVATESEND)) {
781781
itemOutput->setText(COLUMN_PRIVATESEND_ROUNDS, QString::number(nRounds));
782782
} else {

src/qt/walletmodel.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@ int WalletModel::getRealOutpointPrivateSendRounds(const COutPoint& outpoint) con
234234
return wallet->GetRealOutpointPrivateSendRounds(outpoint);
235235
}
236236

237+
bool WalletModel::isFullyMixed(const COutPoint& outpoint) const
238+
{
239+
return wallet->IsFullyMixed(outpoint);
240+
}
241+
237242
void WalletModel::updateAddressBook(const QString &address, const QString &label,
238243
bool isMine, const QString &purpose, int status)
239244
{

src/qt/walletmodel.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ class WalletModel : public QObject
230230
int getNumISLocks() const;
231231

232232
int getRealOutpointPrivateSendRounds(const COutPoint& outpoint) const;
233+
bool isFullyMixed(const COutPoint& outpoint) const;
233234

234235
private:
235236
CWallet *wallet;

src/wallet/rpcwallet.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3188,7 +3188,7 @@ UniValue listunspent(const JSONRPCRequest& request)
31883188
entry.push_back(Pair("spendable", out.fSpendable));
31893189
entry.push_back(Pair("solvable", out.fSolvable));
31903190
entry.push_back(Pair("safe", out.fSafe));
3191-
entry.push_back(Pair("ps_rounds", pwallet->GetCappedOutpointPrivateSendRounds(COutPoint(out.tx->GetHash(), out.i))));
3191+
entry.push_back(Pair("ps_rounds", pwallet->GetRealOutpointPrivateSendRounds(COutPoint(out.tx->GetHash(), out.i))));
31923192
results.push_back(entry);
31933193
}
31943194

src/wallet/wallet.cpp

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,9 +1577,11 @@ int CWallet::GetRealOutpointPrivateSendRounds(const COutPoint& outpoint, int nRo
15771577
{
15781578
LOCK(cs_wallet);
15791579

1580-
if (nRounds >= MAX_PRIVATESEND_ROUNDS) {
1581-
// there can only be MAX_PRIVATESEND_ROUNDS rounds max
1582-
return MAX_PRIVATESEND_ROUNDS - 1;
1580+
const int nRoundsMax = MAX_PRIVATESEND_ROUNDS + privateSendClient.nPrivateSendRandomRounds;
1581+
1582+
if (nRounds >= nRoundsMax) {
1583+
// there can only be nRoundsMax rounds max
1584+
return nRoundsMax - 1;
15831585
}
15841586

15851587
auto pair = mapOutpointRoundsCache.emplace(outpoint, -10);
@@ -1645,7 +1647,7 @@ int CWallet::GetRealOutpointPrivateSendRounds(const COutPoint& outpoint, int nRo
16451647
}
16461648
}
16471649
*nRoundsRef = fDenomFound
1648-
? (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
1650+
? (nShortest >= nRoundsMax - 1 ? nRoundsMax : nShortest + 1) // good, we a +1 to the shortest one but only nRoundsMax rounds max allowed
16491651
: 0; // too bad, we are the fist one in that chain
16501652
LogPrint(BCLog::PRIVATESEND, "%s UPDATED %-70s %3d\n", __func__, outpoint.ToStringShort(), *nRoundsRef);
16511653
return *nRoundsRef;
@@ -1675,6 +1677,29 @@ bool CWallet::IsDenominated(const COutPoint& outpoint) const
16751677
return CPrivateSend::IsDenominatedAmount(it->second.tx->vout[outpoint.n].nValue);
16761678
}
16771679

1680+
bool CWallet::IsFullyMixed(const COutPoint& outpoint) const
1681+
{
1682+
int nRounds = GetRealOutpointPrivateSendRounds(outpoint);
1683+
// Mix again if we don't have N rounds yet
1684+
if (nRounds < privateSendClient.nPrivateSendRounds) return false;
1685+
1686+
// Try to mix a "random" number of rounds more than minimum.
1687+
// If we have already mixed N + MaxOffset rounds, don't mix again.
1688+
// Otherwise, we should mix again 50% of the time, this results in an exponential decay
1689+
// N rounds 50% N+1 25% N+2 12.5%... until we reach N + GetRandomRounds() rounds where we stop.
1690+
if (nRounds < privateSendClient.nPrivateSendRounds + privateSendClient.nPrivateSendRandomRounds) {
1691+
CDataStream ss(SER_GETHASH, PROTOCOL_VERSION);
1692+
ss << outpoint << nPrivateSendSalt;
1693+
uint256 nHash;
1694+
CSHA256().Write((const unsigned char*)ss.data(), ss.size()).Finalize(nHash.begin());
1695+
if (nHash.GetCheapHash() % 2 == 0) {
1696+
return false;
1697+
}
1698+
}
1699+
1700+
return true;
1701+
}
1702+
16781703
isminetype CWallet::IsMine(const CTxOut& txout) const
16791704
{
16801705
return ::IsMine(*this, txout.scriptPubKey);
@@ -2317,8 +2342,7 @@ CAmount CWalletTx::GetAnonymizedCredit(const CCoinControl* coinControl) const
23172342

23182343
if (pwallet->IsSpent(hashTx, i) || !CPrivateSend::IsDenominatedAmount(txout.nValue)) continue;
23192344

2320-
const int nRounds = pwallet->GetCappedOutpointPrivateSendRounds(outpoint);
2321-
if (nRounds >= privateSendClient.nPrivateSendRounds){
2345+
if (pwallet->IsFullyMixed(outpoint)) {
23222346
nCredit += pwallet->GetCredit(txout, ISMINE_SPENDABLE);
23232347
if (!MoneyRange(nCredit))
23242348
throw std::runtime_error(std::string(__func__) + ": value out of range");
@@ -2787,12 +2811,10 @@ void CWallet::AvailableCoins(std::vector<COutput> &vCoins, bool fOnlySafe, const
27872811
bool found = false;
27882812
if (nCoinType == CoinType::ONLY_FULLY_MIXED) {
27892813
if (!CPrivateSend::IsDenominatedAmount(pcoin->tx->vout[i].nValue)) continue;
2790-
int nRounds = GetCappedOutpointPrivateSendRounds(COutPoint(wtxid, i));
2791-
found = nRounds >= privateSendClient.nPrivateSendRounds;
2814+
found = IsFullyMixed(COutPoint(wtxid, i));
27922815
} else if(nCoinType == CoinType::ONLY_READY_TO_MIX) {
27932816
if (!CPrivateSend::IsDenominatedAmount(pcoin->tx->vout[i].nValue)) continue;
2794-
int nRounds = GetCappedOutpointPrivateSendRounds(COutPoint(wtxid, i));
2795-
found = nRounds < privateSendClient.nPrivateSendRounds;
2817+
found = !IsFullyMixed(COutPoint(wtxid, i));
27962818
} else if(nCoinType == CoinType::ONLY_NONDENOMINATED) {
27972819
if (CPrivateSend::IsCollateralAmount(pcoin->tx->vout[i].nValue)) continue; // do not use collateral amounts
27982820
found = !CPrivateSend::IsDenominatedAmount(pcoin->tx->vout[i].nValue);
@@ -2909,6 +2931,21 @@ const CTxOut& CWallet::FindNonChangeParentOutput(const CTransaction& tx, int out
29092931
return ptx->vout[n];
29102932
}
29112933

2934+
void CWallet::InitPrivateSendSalt()
2935+
{
2936+
// Avoid fetching it multiple times
2937+
assert(nPrivateSendSalt.IsNull());
2938+
2939+
WalletBatch batch(*database);
2940+
batch.ReadPrivateSendSalt(nPrivateSendSalt);
2941+
2942+
while (nPrivateSendSalt.IsNull()) {
2943+
// We never generated/saved it
2944+
nPrivateSendSalt = GetRandHash();
2945+
batch.WritePrivateSendSalt(nPrivateSendSalt);
2946+
}
2947+
}
2948+
29122949
static void ApproximateBestSubset(const std::vector<CInputCoin>& vValue, const CAmount& nTotalLower, const CAmount& nTargetValue,
29132950
std::vector<char>& vfBest, CAmount& nBest, int iterations = 1000)
29142951
{
@@ -3174,8 +3211,7 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm
31743211
if (nCoinType == CoinType::ONLY_FULLY_MIXED) {
31753212
// Make sure to include mixed preset inputs only,
31763213
// even if some non-mixed inputs were manually selected via CoinControl
3177-
int nRounds = GetRealOutpointPrivateSendRounds(outpoint);
3178-
if (nRounds < privateSendClient.nPrivateSendRounds) continue;
3214+
if (!IsFullyMixed(outpoint)) continue;
31793215
}
31803216
nValueFromPresetInputs += pcoin->tx->vout[outpoint.n].nValue;
31813217
setPresetCoins.insert(CInputCoin(pcoin, outpoint.n));
@@ -3376,7 +3412,7 @@ bool CWallet::SelectCoinsGroupedByAddresses(std::vector<CompactTallyItem>& vecTa
33763412
// otherwise they will just lead to higher fee / lower priority
33773413
if(wtx.tx->vout[i].nValue <= nSmallestDenom/10) continue;
33783414
// ignore mixed
3379-
if(GetCappedOutpointPrivateSendRounds(COutPoint(outpoint.hash, i)) >= privateSendClient.nPrivateSendRounds) continue;
3415+
if (IsFullyMixed(COutPoint(outpoint.hash, i))) continue;
33803416
}
33813417

33823418
if (itTallyItem == mapTally.end()) {
@@ -4155,6 +4191,8 @@ DBErrors CWallet::LoadWallet(bool& fFirstRunRet)
41554191
}
41564192
}
41574193

4194+
InitPrivateSendSalt();
4195+
41584196
if (nLoadWalletRet != DB_LOAD_OK)
41594197
return nLoadWalletRet;
41604198

src/wallet/wallet.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,17 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface
817817
*/
818818
const CBlockIndex* m_last_block_processed;
819819

820+
/** Pulled from wallet DB ("ps_salt") and used when mixing a random number of rounds.
821+
* This salt is needed to prevent an attacker from learning how many extra times
822+
* the input was mixed based only on information in the blockchain.
823+
*/
824+
uint256 nPrivateSendSalt;
825+
826+
/**
827+
* Fetches PrivateSend salt from database or generates and saves a new one if no salt was found in the db
828+
*/
829+
void InitPrivateSendSalt();
830+
820831
public:
821832
/*
822833
* Main wallet lock.
@@ -949,6 +960,7 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface
949960
int GetCappedOutpointPrivateSendRounds(const COutPoint& outpoint) const;
950961

951962
bool IsDenominated(const COutPoint& outpoint) const;
963+
bool IsFullyMixed(const COutPoint& outpoint) const;
952964

953965
bool IsSpent(const uint256& hash, unsigned int n) const;
954966

src/wallet/walletdb.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@ bool WalletBatch::WriteAccountingEntry(const uint64_t nAccEntryNum, const CAccou
168168
return WriteIC(std::make_pair(std::string("acentry"), std::make_pair(acentry.strAccount, nAccEntryNum)), acentry);
169169
}
170170

171+
bool WalletBatch::ReadPrivateSendSalt(uint256& salt)
172+
{
173+
return m_batch.Read(std::string("ps_salt"), salt);
174+
}
175+
176+
bool WalletBatch::WritePrivateSendSalt(const uint256& salt)
177+
{
178+
return WriteIC(std::string("ps_salt"), salt);
179+
}
180+
171181
CAmount WalletBatch::GetAccountCreditDebit(const std::string& strAccount)
172182
{
173183
std::list<CAccountingEntry> entries;

src/wallet/walletdb.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ class WalletBatch
160160
bool ReadAccount(const std::string& strAccount, CAccount& account);
161161
bool WriteAccount(const std::string& strAccount, const CAccount& account);
162162

163+
bool ReadPrivateSendSalt(uint256& salt);
164+
bool WritePrivateSendSalt(const uint256& salt);
165+
163166
/// Write destination data key,value tuple to database
164167
bool WriteDestData(const std::string &address, const std::string &key, const std::string &value);
165168
/// Erase destination data tuple from wallet database

0 commit comments

Comments
 (0)