Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 46 additions & 13 deletions src/privatesend-client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ void CPrivateSendClient::ProcessMessage(CNode* pfrom, const std::string& strComm
LogPrint("privatesend", "DSQUEUE -- %s new\n", dsq.ToString());

if(dsq.IsExpired()) return;
if(dsq.nInputCount < 0 || dsq.nInputCount > PRIVATESEND_ENTRY_MAX_SIZE) return;

masternode_info_t infoMn;
if(!mnodeman.GetMasternodeInfo(dsq.masternodeOutpoint, infoMn)) return;
Expand Down Expand Up @@ -875,10 +876,18 @@ bool CPrivateSendClient::JoinExistingQueue(CAmount nBalanceNeedsAnonymized, CCon
CAmount nValueInTmp = 0;
std::vector<CTxDSIn> vecTxDSInTmp;
std::vector<COutput> vCoinsTmp;

// Try to match their denominations if possible, select at least 1 denominations
if(!pwalletMain->SelectCoinsByDenominations(dsq.nDenom, vecStandardDenoms[vecBits.front()], nBalanceNeedsAnonymized, vecTxDSInTmp, vCoinsTmp, nValueInTmp, 0, nPrivateSendRounds)) {
LogPrintf("CPrivateSendClient::JoinExistingQueue -- Couldn't match denominations %d %d (%s)\n", vecBits.front(), dsq.nDenom, CPrivateSend::GetDenominationsToString(dsq.nDenom));
int nMinAmount = vecStandardDenoms[vecBits.front()];
int nMaxAmount = nBalanceNeedsAnonymized;
// nInputCount is not covered by legacy signature, require SPORK_6_NEW_SIGS to activate to use new algo
// (to make sure nInputCount wasn't modified by some intermediary node)
bool fNewAlgo = infoMn.nProtocolVersion > 70208 && sporkManager.IsSporkActive(SPORK_6_NEW_SIGS);

if (fNewAlgo && dsq.nInputCount != 0) {
nMinAmount = nMaxAmount = dsq.nInputCount * vecStandardDenoms[vecBits.front()];
}
// Try to match their denominations if possible, select exact number of denominations
if(!pwalletMain->SelectCoinsByDenominations(dsq.nDenom, nMinAmount, nMaxAmount, vecTxDSInTmp, vCoinsTmp, nValueInTmp, 0, nPrivateSendRounds)) {
LogPrintf("CPrivateSendClient::JoinExistingQueue -- Couldn't match %d denominations %d %d (%s)\n", dsq.nInputCount, vecBits.front(), dsq.nDenom, CPrivateSend::GetDenominationsToString(dsq.nDenom));
continue;
}

Expand All @@ -890,14 +899,15 @@ bool CPrivateSendClient::JoinExistingQueue(CAmount nBalanceNeedsAnonymized, CCon
}

nSessionDenom = dsq.nDenom;
nSessionInputCount = fNewAlgo ? dsq.nInputCount : 0;
infoMixingMasternode = infoMn;
pendingDsaRequest = CPendingDsaRequest(infoMn.addr, CDarksendAccept(nSessionDenom, txMyCollateral));
pendingDsaRequest = CPendingDsaRequest(infoMn.addr, CDarksendAccept(nSessionDenom, nSessionInputCount, txMyCollateral));
connman.AddPendingMasternode(infoMn.addr);
// TODO: add new state POOL_STATE_CONNECTING and bump MIN_PRIVATESEND_PEER_PROTO_VERSION
SetState(POOL_STATE_QUEUE);
nTimeLastSuccessfulStep = GetTime();
LogPrintf("CPrivateSendClient::JoinExistingQueue -- pending connection (from queue): nSessionDenom: %d (%s), addr=%s\n",
nSessionDenom, CPrivateSend::GetDenominationsToString(nSessionDenom), infoMn.addr.ToString());
LogPrintf("CPrivateSendClient::JoinExistingQueue -- pending connection (from queue): nSessionDenom: %d (%s), nSessionInputCount: %d, addr=%s\n",
nSessionDenom, CPrivateSend::GetDenominationsToString(nSessionDenom), nSessionInputCount, infoMn.addr.ToString());
strAutoDenomResult = _("Trying to connect...");
return true;
}
Expand Down Expand Up @@ -963,14 +973,37 @@ bool CPrivateSendClient::StartNewQueue(CAmount nValueMin, CAmount nBalanceNeedsA
nSessionDenom = CPrivateSend::GetDenominationsByAmounts(vecAmounts);
}

// Count available denominations.
// Should never really fail after this point, since we just selected compatible inputs ourselves.
std::vector<int> vecBits;
if (!CPrivateSend::GetDenominationsBits(nSessionDenom, vecBits)) {
return false;
}

CAmount nValueInTmp = 0;
std::vector<CTxDSIn> vecTxDSInTmp;
std::vector<COutput> vCoinsTmp;
std::vector<CAmount> vecStandardDenoms = CPrivateSend::GetStandardDenominations();

bool fSelected = pwalletMain->SelectCoinsByDenominations(nSessionDenom, vecStandardDenoms[vecBits.front()], vecStandardDenoms[vecBits.front()] * PRIVATESEND_ENTRY_MAX_SIZE, vecTxDSInTmp, vCoinsTmp, nValueInTmp, 0, nPrivateSendRounds);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and in JoinExistingQueue you do selection for whole range of PS rounds. And you assume this amount of inputs to set nInputCount (or to check in JoinExistingQueue)

But the more value of nInputCount, the more likely failure of this code, since in PrepareDenominate selection is limited to only one PS round.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's a good point but the code that follows the one you pointed to covers such cases, so shouldn't be an issue imo.

Copy link

@InhumanPerfection InhumanPerfection Jun 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, but now mixing will be complete mess:

  • in some cases will be selected inputs with highest number of rounds
  • in some cases will be selected inputs with different(!) number of rounds

Edit: however I'm not sure is it good or bad for privacy ;-)

Copy link
Author

@UdjinM6 UdjinM6 Jun 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see what you mean. But the former one is just the optimal strategy to get some PS balance quicker, while the later one is still a good one because it either advances low PS rounds inputs (which will help to get some more PS balance later) or mixes inputs which already have enough rounds and this shouldn't hurt either.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or am I missing some another/new vulnerability here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the best possible privacy, the inputs likely should be selected randomly. Any other setup has the potential for data leakage and trace-ability, maybe there is a nice way to make it look random while also maintaining speed of getting a balance. However, the random setup would also have a problem for single input private-sending, unless there was very high liquidity of single input only mixing. Maybe we need to say you can't only mix one input?

However, having it be that "in some cases inputs will be selected with highest number of rounds" is likely a very bad idea as there will likely be grouping (when you look at Redeemed in and previous output) happening allowing for the breaking of a mix.

Copy link

@Antti-Kaikkonen Antti-Kaikkonen Jun 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with PaulieD. If the inputs were selected randomly then I think you couldn't really deduce this:

other 2 participants each have 3 inputs from other mixing transactions (txY and txZ)

. But if the most recent inputs are always used then I think that InhumanPerfection is correct.

I don't really understand the original issue. If someone could come up with an example scenario where the privacy is reduced by participants using a different number of inputs, then I might be able to understand it. The way I see it currently is that since a blockchain observer shouldn't be able to know which inputs belong to which mixing participant, then the observer also shouldn't be able to tell how many inputs belong to each participant.

Copy link

@Antti-Kaikkonen Antti-Kaikkonen Jun 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paulied Do you think prioritizing inputs with the highest number of rounds might have also caused #2092 or made it significantly more common?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can confirm that there is a definite problem that this pr should fix. @UdjinM6 might be able to provide more details.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Antti-Kaikkonen yeah, if the selections were random you certainly wouldn't see that

if (!fSelected) {
return false;
}

// nInputCount is not covered by legacy signature, require SPORK_6_NEW_SIGS to activate to use new algo
// (to make sure nInputCount wasn't modified by some intermediary node)
bool fNewAlgo = infoMn.nProtocolVersion > 70208 && sporkManager.IsSporkActive(SPORK_6_NEW_SIGS);
nSessionInputCount = fNewAlgo
? std::min(vecTxDSInTmp.size(), size_t(5 + GetRand(PRIVATESEND_ENTRY_MAX_SIZE - 5 + 1)))
: 0;
infoMixingMasternode = infoMn;
connman.AddPendingMasternode(infoMn.addr);
pendingDsaRequest = CPendingDsaRequest(infoMn.addr, CDarksendAccept(nSessionDenom, txMyCollateral));
pendingDsaRequest = CPendingDsaRequest(infoMn.addr, CDarksendAccept(nSessionDenom, nSessionInputCount, txMyCollateral));
// TODO: add new state POOL_STATE_CONNECTING and bump MIN_PRIVATESEND_PEER_PROTO_VERSION
SetState(POOL_STATE_QUEUE);
nTimeLastSuccessfulStep = GetTime();
LogPrintf("CPrivateSendClient::StartNewQueue -- pending connection, nSessionDenom: %d (%s), addr=%s\n",
nSessionDenom, CPrivateSend::GetDenominationsToString(nSessionDenom), infoMn.addr.ToString());
LogPrintf("CPrivateSendClient::StartNewQueue -- pending connection, nSessionDenom: %d (%s), nSessionInputCount: %d, addr=%s\n",
nSessionDenom, CPrivateSend::GetDenominationsToString(nSessionDenom), nSessionInputCount, infoMn.addr.ToString());
strAutoDenomResult = _("Trying to connect...");
return true;
}
Expand Down Expand Up @@ -1077,7 +1110,7 @@ bool CPrivateSendClient::PrepareDenominate(int nMinRounds, int nMaxRounds, std::
return false;
}
std::vector<CAmount> vecStandardDenoms = CPrivateSend::GetStandardDenominations();
bool fSelected = pwalletMain->SelectCoinsByDenominations(nSessionDenom, vecStandardDenoms[vecBits.front()], CPrivateSend::GetMaxPoolAmount(), vecTxDSIn, vCoins, nValueIn, nMinRounds, nMaxRounds);
bool fSelected = pwalletMain->SelectCoinsByDenominations(nSessionDenom, vecStandardDenoms[vecBits.front()], vecStandardDenoms[vecBits.front()] * PRIVATESEND_ENTRY_MAX_SIZE, vecTxDSIn, vCoins, nValueIn, nMinRounds, nMaxRounds);
if (nMinRounds >= 0 && !fSelected) {
strErrorRet = "Can't select current denominated inputs";
return false;
Expand All @@ -1098,7 +1131,7 @@ bool CPrivateSendClient::PrepareDenominate(int nMinRounds, int nMaxRounds, std::
// NOTE: No need to randomize order of inputs because they were
// initially shuffled in CWallet::SelectCoinsByDenominations already.
int nStep = 0;
int nStepsMax = 5 + GetRandInt(PRIVATESEND_ENTRY_MAX_SIZE-5+1);
int nStepsMax = nSessionInputCount != 0 ? nSessionInputCount : (5 + GetRandInt(PRIVATESEND_ENTRY_MAX_SIZE - 5 + 1));

while (nStep < nStepsMax) {
for (const auto& nBit : vecBits) {
Expand Down Expand Up @@ -1145,7 +1178,7 @@ bool CPrivateSendClient::PrepareDenominate(int nMinRounds, int nMaxRounds, std::
}
}

if (CPrivateSend::GetDenominations(vecTxOutRet) != nSessionDenom) {
if (CPrivateSend::GetDenominations(vecTxOutRet) != nSessionDenom || (nSessionInputCount != 0 && nStep != nStepsMax)) {
{
// unlock used coins on failure
LOCK(pwalletMain->cs_wallet);
Expand Down
39 changes: 35 additions & 4 deletions src/privatesend-server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ void CPrivateSendServer::ProcessMessage(CNode* pfrom, const std::string& strComm

LogPrint("privatesend", "DSACCEPT -- nDenom %d (%s) txCollateral %s", dsa.nDenom, CPrivateSend::GetDenominationsToString(dsa.nDenom), dsa.txCollateral.ToString());

if(dsa.nInputCount < 0 || dsa.nInputCount > PRIVATESEND_ENTRY_MAX_SIZE) return;

masternode_info_t mnInfo;
if(!mnodeman.GetMasternodeInfo(activeMasternode.outpoint, mnInfo)) {
PushStatus(pfrom, STATUS_REJECTED, ERR_MN_LIST, connman);
Expand Down Expand Up @@ -98,6 +100,7 @@ void CPrivateSendServer::ProcessMessage(CNode* pfrom, const std::string& strComm
LogPrint("privatesend", "DSQUEUE -- %s new\n", dsq.ToString());

if(dsq.IsExpired()) return;
if(dsq.nInputCount < 0 || dsq.nInputCount > PRIVATESEND_ENTRY_MAX_SIZE) return;

masternode_info_t mnInfo;
if(!mnodeman.GetMasternodeInfo(dsq.masternodeOutpoint, mnInfo)) return;
Expand Down Expand Up @@ -165,6 +168,18 @@ void CPrivateSendServer::ProcessMessage(CNode* pfrom, const std::string& strComm
return;
}

if(nSessionInputCount != 0 && entry.vecTxDSIn.size() != nSessionInputCount) {
LogPrintf("DSVIN -- ERROR: incorrect number of inputs! %d/%d\n", entry.vecTxDSIn.size(), nSessionInputCount);
PushStatus(pfrom, STATUS_REJECTED, ERR_INVALID_INPUT_COUNT, connman);
return;
}

if(nSessionInputCount != 0 && entry.vecTxOut.size() != nSessionInputCount) {
LogPrintf("DSVIN -- ERROR: incorrect number of outputs! %d/%d\n", entry.vecTxOut.size(), nSessionInputCount);
PushStatus(pfrom, STATUS_REJECTED, ERR_INVALID_INPUT_COUNT, connman);
return;
}

//do we have the same denominations as the current session?
if(!IsOutputsCompatibleWithSessionDenom(entry.vecTxOut)) {
LogPrintf("DSVIN -- not compatible with existing transactions!\n");
Expand Down Expand Up @@ -492,7 +507,7 @@ void CPrivateSendServer::CheckForCompleteQueue(CConnman& connman)
if(nState == POOL_STATE_QUEUE && IsSessionReady()) {
SetState(POOL_STATE_ACCEPTING_ENTRIES);

CDarksendQueue dsq(nSessionDenom, activeMasternode.outpoint, GetAdjustedTime(), true);
CDarksendQueue dsq(nSessionDenom, nSessionInputCount, activeMasternode.outpoint, GetAdjustedTime(), true);
LogPrint("privatesend", "CPrivateSendServer::CheckForCompleteQueue -- queue is ready, signing and relaying (%s)\n", dsq.ToString());
dsq.Sign();
dsq.Relay(connman);
Expand Down Expand Up @@ -670,6 +685,12 @@ bool CPrivateSendServer::IsAcceptableDSA(const CDarksendAccept& dsa, PoolMessage
return false;
}

if(dsa.nInputCount < 0 || dsa.nInputCount > PRIVATESEND_ENTRY_MAX_SIZE) {
LogPrint("privatesend", "CPrivateSendServer::%s -- requested count is not valid!\n", __func__);
nMessageIDRet = ERR_INVALID_INPUT_COUNT;
return false;
}

return true;
}

Expand All @@ -692,13 +713,16 @@ bool CPrivateSendServer::CreateNewSession(const CDarksendAccept& dsa, PoolMessag
nMessageIDRet = MSG_NOERR;
nSessionID = GetRandInt(999999)+1;
nSessionDenom = dsa.nDenom;
// nInputCount is not covered by legacy signature, require SPORK_6_NEW_SIGS to activate to use new algo
// (to make sure nInputCount wasn't modified by some intermediary node)
nSessionInputCount = sporkManager.IsSporkActive(SPORK_6_NEW_SIGS) ? dsa.nInputCount : 0;

SetState(POOL_STATE_QUEUE);
nTimeLastSuccessfulStep = GetTime();

if(!fUnitTest) {
//broadcast that I'm accepting entries, only if it's the first entry through
CDarksendQueue dsq(dsa.nDenom, activeMasternode.outpoint, GetAdjustedTime(), false);
CDarksendQueue dsq(dsa.nDenom, dsa.nInputCount, activeMasternode.outpoint, GetAdjustedTime(), false);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nSessionInputCount instead of dsa.nInputCount here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the way it's activated this shouldn't really matter but I agree that using nSession* here would improve readability.

LogPrint("privatesend", "CPrivateSendServer::CreateNewSession -- signing and relaying new queue: %s\n", dsq.ToString());
dsq.Sign();
dsq.Relay(connman);
Expand Down Expand Up @@ -734,14 +758,21 @@ bool CPrivateSendServer::AddUserToExistingSession(const CDarksendAccept& dsa, Po
return false;
}

if(dsa.nInputCount != nSessionInputCount) {
LogPrintf("CPrivateSendServer::AddUserToExistingSession -- incompatible count %d != nSessionInputCount %d\n",
dsa.nInputCount, nSessionInputCount);
nMessageIDRet = ERR_INVALID_INPUT_COUNT;
return false;
}

// count new user as accepted to an existing session

nMessageIDRet = MSG_NOERR;
nTimeLastSuccessfulStep = GetTime();
vecSessionCollaterals.push_back(MakeTransactionRef(dsa.txCollateral));

LogPrintf("CPrivateSendServer::AddUserToExistingSession -- new user accepted, nSessionID: %d nSessionDenom: %d (%s) vecSessionCollaterals.size(): %d\n",
nSessionID, nSessionDenom, CPrivateSend::GetDenominationsToString(nSessionDenom), vecSessionCollaterals.size());
LogPrintf("CPrivateSendServer::AddUserToExistingSession -- new user accepted, nSessionID: %d nSessionDenom: %d (%s) nSessionInputCount: %d vecSessionCollaterals.size(): %d\n",
nSessionID, nSessionDenom, CPrivateSend::GetDenominationsToString(nSessionDenom), nSessionInputCount, vecSessionCollaterals.size());

return true;
}
Expand Down
2 changes: 2 additions & 0 deletions src/privatesend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ void CPrivateSendBase::SetNull()
nState = POOL_STATE_IDLE;
nSessionID = 0;
nSessionDenom = 0;
nSessionInputCount = 0;
vecEntries.clear();
finalMutableTransaction.vin.clear();
finalMutableTransaction.vout.clear();
Expand Down Expand Up @@ -457,6 +458,7 @@ std::string CPrivateSend::GetMessageByID(PoolMessage nMessageID)
case MSG_NOERR: return _("No errors detected.");
case MSG_SUCCESS: return _("Transaction created successfully.");
case MSG_ENTRIES_ADDED: return _("Your entries added successfully.");
case ERR_INVALID_INPUT_COUNT: return _("Invalid input count.");
default: return _("Unknown response.");
}
}
Expand Down
31 changes: 25 additions & 6 deletions src/privatesend.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ static const int PRIVATESEND_SIGNING_TIMEOUT = 15;
//! minimum peer version accepted by mixing pool
static const int MIN_PRIVATESEND_PEER_PROTO_VERSION = 70208;

static const CAmount PRIVATESEND_ENTRY_MAX_SIZE = 9;
static const size_t PRIVATESEND_ENTRY_MAX_SIZE = 9;

// pool responses
enum PoolMessage {
Expand All @@ -51,6 +51,7 @@ enum PoolMessage {
MSG_NOERR,
MSG_SUCCESS,
MSG_ENTRIES_ADDED,
ERR_INVALID_INPUT_COUNT,
MSG_POOL_MIN = ERR_ALREADY_HAVE,
MSG_POOL_MAX = MSG_ENTRIES_ADDED
};
Expand Down Expand Up @@ -99,15 +100,18 @@ class CDarksendAccept
{
public:
int nDenom;
int nInputCount;
CMutableTransaction txCollateral;

CDarksendAccept() :
nDenom(0),
nInputCount(0),
txCollateral(CMutableTransaction())
{};

CDarksendAccept(int nDenom, const CMutableTransaction& txCollateral) :
CDarksendAccept(int nDenom, int nInputCount, const CMutableTransaction& txCollateral) :
nDenom(nDenom),
nInputCount(nInputCount),
txCollateral(txCollateral)
{};

Expand All @@ -116,6 +120,12 @@ class CDarksendAccept
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action) {
READWRITE(nDenom);
int nVersion = s.GetVersion();
if (nVersion > 70208) {
READWRITE(nInputCount);
} else if (ser_action.ForRead()) {
nInputCount = 0;
}
READWRITE(txCollateral);
}

Expand Down Expand Up @@ -169,6 +179,7 @@ class CDarksendQueue
{
public:
int nDenom;
int nInputCount;
COutPoint masternodeOutpoint;
int64_t nTime;
bool fReady; //ready for submit
Expand All @@ -178,15 +189,17 @@ class CDarksendQueue

CDarksendQueue() :
nDenom(0),
nInputCount(0),
masternodeOutpoint(COutPoint()),
nTime(0),
fReady(false),
vchSig(std::vector<unsigned char>()),
fTried(false)
{}

CDarksendQueue(int nDenom, COutPoint outpoint, int64_t nTime, bool fReady) :
CDarksendQueue(int nDenom, int nInputCount, COutPoint outpoint, int64_t nTime, bool fReady) :
nDenom(nDenom),
nInputCount(nInputCount),
masternodeOutpoint(outpoint),
nTime(nTime),
fReady(fReady),
Expand All @@ -200,6 +213,11 @@ class CDarksendQueue
inline void SerializationOp(Stream& s, Operation ser_action) {
READWRITE(nDenom);
int nVersion = s.GetVersion();
if (nVersion > 70208) {
READWRITE(nInputCount);
} else if (ser_action.ForRead()) {
nInputCount = 0;
}
if (nVersion == 70208 && (s.GetType() & SER_NETWORK)) {
// converting from/to old format
CTxIn txin{};
Expand Down Expand Up @@ -240,13 +258,13 @@ class CDarksendQueue

std::string ToString() const
{
return strprintf("nDenom=%d, nTime=%lld, fReady=%s, fTried=%s, masternode=%s",
nDenom, nTime, fReady ? "true" : "false", fTried ? "true" : "false", masternodeOutpoint.ToStringShort());
return strprintf("nDenom=%d, nInputCount=%d, nTime=%lld, fReady=%s, fTried=%s, masternode=%s",
nDenom, nInputCount, nTime, fReady ? "true" : "false", fTried ? "true" : "false", masternodeOutpoint.ToStringShort());
}

friend bool operator==(const CDarksendQueue& a, const CDarksendQueue& b)
{
return a.nDenom == b.nDenom && a.masternodeOutpoint == b.masternodeOutpoint && a.nTime == b.nTime && a.fReady == b.fReady;
return a.nDenom == b.nDenom && a.nInputCount == b.nInputCount && a.masternodeOutpoint == b.masternodeOutpoint && a.nTime == b.nTime && a.fReady == b.fReady;
}
};

Expand Down Expand Up @@ -352,6 +370,7 @@ class CPrivateSendBase

public:
int nSessionDenom; //Users must submit an denom matching this
int nSessionInputCount; //Users must submit a count matching this

CPrivateSendBase() { SetNull(); }

Expand Down