// Copyright (c) 2014-2015 The Dash developers // Copyright (c) 2015-2019 The PIVX developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "obfuscation.h" #include "coincontrol.h" #include "init.h" #include "main.h" #include "masternodeman.h" #include "script/sign.h" #include "swifttx.h" #include "guiinterface.h" #include "util.h" #include #include #include #include #include #include #include using namespace std; using namespace boost; // The main object for accessing Obfuscation CObfuscationPool obfuScationPool; // A helper object for signing messages from Masternodes CObfuScationSigner obfuScationSigner; // The current Obfuscations in progress on the network std::vector vecObfuscationQueue; // Keep track of the used Masternodes std::vector vecMasternodesUsed; // Keep track of the scanning errors I've seen map mapObfuscationBroadcastTxes; // Keep track of the active Masternode CActiveMasternode activeMasternode; int randomizeList(int i) { return std::rand() % i; } void CObfuscationPool::Reset() { cachedLastSuccess = 0; lastNewBlock = 0; vecMasternodesUsed.clear(); UnlockCoins(); SetNull(); } void CObfuscationPool::SetNull() { // MN side sessionUsers = 0; vecSessionCollateral.clear(); // Client side entriesCount = 0; lastEntryAccepted = 0; countEntriesAccepted = 0; // Both sides state = POOL_STATUS_IDLE; sessionID = 0; sessionDenom = 0; entries.clear(); finalTransaction.vin.clear(); finalTransaction.vout.clear(); lastTimeChanged = GetTimeMillis(); // -- seed random number generator (used for ordering output lists) unsigned int seed = 0; RAND_bytes((unsigned char*)&seed, sizeof(seed)); std::srand(seed); } bool CObfuscationPool::SetCollateralAddress(std::string strAddress) { CBitcoinAddress address; if (!address.SetString(strAddress)) { LogPrintf("CObfuscationPool::SetCollateralAddress - Invalid Obfuscation collateral address\n"); return false; } collateralPubKey = GetScriptForDestination(address.Get()); return true; } // // Unlock coins after Obfuscation fails or succeeds // void CObfuscationPool::UnlockCoins() { if (!pwalletMain) return; while (true) { TRY_LOCK(pwalletMain->cs_wallet, lockWallet); if (!lockWallet) { MilliSleep(50); continue; } for (CTxIn v : lockedCoins) pwalletMain->UnlockCoin(v.prevout); break; } lockedCoins.clear(); } // // Check the Obfuscation progress and send client updates if a Masternode // void CObfuscationPool::Check() { if (fMasterNode) LogPrint("obfuscation", "CObfuscationPool::Check() - entries count %lu\n", entries.size()); //printf("CObfuscationPool::Check() %d - %d - %d\n", state, anonTx.CountEntries(), GetTimeMillis()-lastTimeChanged); if (fMasterNode) { LogPrint("obfuscation", "CObfuscationPool::Check() - entries count %lu\n", entries.size()); // If entries is full, then move on to the next phase if (state == POOL_STATUS_ACCEPTING_ENTRIES && (int)entries.size() >= GetMaxPoolTransactions()) { LogPrint("obfuscation", "CObfuscationPool::Check() -- TRYING TRANSACTION \n"); UpdateState(POOL_STATUS_FINALIZE_TRANSACTION); } } // create the finalized transaction for distribution to the clients if (state == POOL_STATUS_FINALIZE_TRANSACTION) { LogPrint("obfuscation", "CObfuscationPool::Check() -- FINALIZE TRANSACTIONS\n"); UpdateState(POOL_STATUS_SIGNING); if (fMasterNode) { CMutableTransaction txNew; // make our new transaction for (unsigned int i = 0; i < entries.size(); i++) { for (const CTxOut& v : entries[i].vout) txNew.vout.push_back(v); for (const CTxDSIn& s : entries[i].sev) txNew.vin.push_back(s); } // shuffle the outputs for improved anonymity std::random_shuffle(txNew.vin.begin(), txNew.vin.end(), randomizeList); std::random_shuffle(txNew.vout.begin(), txNew.vout.end(), randomizeList); LogPrint("obfuscation", "Transaction 1: %s\n", txNew.ToString()); finalTransaction = txNew; // request signatures from clients RelayFinalTransaction(sessionID, finalTransaction); } } // If we have all of the signatures, try to compile the transaction if (fMasterNode && state == POOL_STATUS_SIGNING && SignaturesComplete()) { LogPrint("obfuscation", "CObfuscationPool::Check() -- SIGNING\n"); UpdateState(POOL_STATUS_TRANSMISSION); CheckFinalTransaction(); } // reset if we're here for 10 seconds if ((state == POOL_STATUS_ERROR || state == POOL_STATUS_SUCCESS) && GetTimeMillis() - lastTimeChanged >= 10000) { LogPrint("obfuscation", "CObfuscationPool::Check() -- timeout, RESETTING\n"); UnlockCoins(); SetNull(); if (fMasterNode) RelayStatus(sessionID, GetState(), GetEntriesCount(), MASTERNODE_RESET); } } void CObfuscationPool::CheckFinalTransaction() { if (!fMasterNode) return; // check and relay final tx only on masternode CWalletTx txNew = CWalletTx(pwalletMain, finalTransaction); LOCK2(cs_main, pwalletMain->cs_wallet); { LogPrint("obfuscation", "Transaction 2: %s\n", txNew.ToString()); // See if the transaction is valid if (!txNew.AcceptToMemoryPool(false, true, true)) { LogPrintf("CObfuscationPool::Check() - CommitTransaction : Error: Transaction not valid\n"); SetNull(); // not much we can do in this case UpdateState(POOL_STATUS_ACCEPTING_ENTRIES); RelayCompletedTransaction(sessionID, true, ERR_INVALID_TX); return; } LogPrintf("CObfuscationPool::Check() -- IS MASTER -- TRANSMITTING OBFUSCATION\n"); // sign a message int64_t sigTime = GetAdjustedTime(); std::string strMessage = txNew.GetHash().ToString() + std::to_string(sigTime); std::string strError = ""; std::vector vchSig; CKey key2; CPubKey pubkey2; if (!obfuScationSigner.SetKey(strMasterNodePrivKey, strError, key2, pubkey2)) { LogPrintf("CObfuscationPool::Check() - ERROR: Invalid Masternodeprivkey: '%s'\n", strError); return; } if (!obfuScationSigner.SignMessage(strMessage, strError, vchSig, key2)) { LogPrintf("CObfuscationPool::Check() - Sign message failed\n"); return; } if (!obfuScationSigner.VerifyMessage(pubkey2, vchSig, strMessage, strError)) { LogPrintf("CObfuscationPool::Check() - Verify message failed\n"); return; } if (!mapObfuscationBroadcastTxes.count(txNew.GetHash())) { CObfuscationBroadcastTx dstx; dstx.tx = txNew; dstx.vin = activeMasternode.vin; dstx.vchSig = vchSig; dstx.sigTime = sigTime; mapObfuscationBroadcastTxes.insert(make_pair(txNew.GetHash(), dstx)); } CInv inv(MSG_DSTX, txNew.GetHash()); RelayInv(inv); // Tell the clients it was successful RelayCompletedTransaction(sessionID, false, MSG_SUCCESS); // Randomly charge clients ChargeRandomFees(); // Reset LogPrint("obfuscation", "CObfuscationPool::Check() -- COMPLETED -- RESETTING\n"); SetNull(); RelayStatus(sessionID, GetState(), GetEntriesCount(), MASTERNODE_RESET); } } // // Charge clients a fee if they're abusive // // Why bother? Obfuscation uses collateral to ensure abuse to the process is kept to a minimum. // The submission and signing stages in Obfuscation are completely separate. In the cases where // a client submits a transaction then refused to sign, there must be a cost. Otherwise they // would be able to do this over and over again and bring the mixing to a hault. // // How does this work? Messages to Masternodes come in via "dsi", these require a valid collateral // transaction for the client to be able to enter the pool. This transaction is kept by the Masternode // until the transaction is either complete or fails. // void CObfuscationPool::ChargeFees() { if (!fMasterNode) return; //we don't need to charge collateral for every offence. int offences = 0; int r = rand() % 100; if (r > 33) return; if (state == POOL_STATUS_ACCEPTING_ENTRIES) { for (const CTransaction& txCollateral : vecSessionCollateral) { bool found = false; for (const CObfuScationEntry& v : entries) { if (v.collateral == txCollateral) { found = true; } } // This queue entry didn't send us the promised transaction if (!found) { LogPrintf("CObfuscationPool::ChargeFees -- found uncooperative node (didn't send transaction). Found offence.\n"); offences++; } } } if (state == POOL_STATUS_SIGNING) { // who didn't sign? for (const CObfuScationEntry &v : entries) { for (const CTxDSIn &s : v.sev) { if (!s.fHasSig) { LogPrintf("CObfuscationPool::ChargeFees -- found uncooperative node (didn't sign). Found offence\n"); offences++; } } } } r = rand() % 100; int target = 0; //mostly offending? if (offences >= Params().PoolMaxTransactions() - 1 && r > 33) return; //everyone is an offender? That's not right if (offences >= Params().PoolMaxTransactions()) return; //charge one of the offenders randomly if (offences > 1) target = 50; //pick random client to charge r = rand() % 100; if (state == POOL_STATUS_ACCEPTING_ENTRIES) { for (const CTransaction& txCollateral : vecSessionCollateral) { bool found = false; for (const CObfuScationEntry& v : entries) { if (v.collateral == txCollateral) { found = true; } } // This queue entry didn't send us the promised transaction if (!found && r > target) { LogPrintf("CObfuscationPool::ChargeFees -- found uncooperative node (didn't send transaction). charging fees.\n"); CWalletTx wtxCollateral = CWalletTx(pwalletMain, txCollateral); // Broadcast if (!wtxCollateral.AcceptToMemoryPool(true)) { // This must not fail. The transaction has already been signed and recorded. LogPrintf("CObfuscationPool::ChargeFees() : Error: Transaction not valid"); } wtxCollateral.RelayWalletTransaction(); return; } } } if (state == POOL_STATUS_SIGNING) { // who didn't sign? for (const CObfuScationEntry &v : entries) { for (const CTxDSIn &s : v.sev) { if (!s.fHasSig && r > target) { LogPrintf("CObfuscationPool::ChargeFees -- found uncooperative node (didn't sign). charging fees.\n"); CWalletTx wtxCollateral = CWalletTx(pwalletMain, v.collateral); // Broadcast if (!wtxCollateral.AcceptToMemoryPool(false)) { // This must not fail. The transaction has already been signed and recorded. LogPrintf("CObfuscationPool::ChargeFees() : Error: Transaction not valid"); } wtxCollateral.RelayWalletTransaction(); return; } } } } } // charge the collateral randomly // - Obfuscation is completely free, to pay miners we randomly pay the collateral of users. void CObfuscationPool::ChargeRandomFees() { if (fMasterNode) { int i = 0; for (const CTransaction& txCollateral : vecSessionCollateral) { int r = rand() % 100; /* Collateral Fee Charges: Being that Obfuscation has "no fees" we need to have some kind of cost associated with using it to stop abuse. Otherwise it could serve as an attack vector and allow endless transaction that would bloat Agrarian and make it unusable. To stop these kinds of attacks 1 in 10 successful transactions are charged. This adds up to a cost of 0.001 AGR per transaction on average. */ if (r <= 10) { LogPrintf("CObfuscationPool::ChargeRandomFees -- charging random fees. %u\n", i); CWalletTx wtxCollateral = CWalletTx(pwalletMain, txCollateral); // Broadcast if (!wtxCollateral.AcceptToMemoryPool(true)) { // This must not fail. The transaction has already been signed and recorded. LogPrintf("CObfuscationPool::ChargeRandomFees() : Error: Transaction not valid"); } wtxCollateral.RelayWalletTransaction(); } } } } // // Check for various timeouts (queue objects, Obfuscation, etc) // void CObfuscationPool::CheckTimeout() { if (!fEnableZeromint && !fMasterNode) return; // catching hanging sessions if (!fMasterNode) { switch (state) { case POOL_STATUS_TRANSMISSION: LogPrint("obfuscation", "CObfuscationPool::CheckTimeout() -- Session complete -- Running Check()\n"); Check(); break; case POOL_STATUS_ERROR: LogPrint("obfuscation", "CObfuscationPool::CheckTimeout() -- Pool error -- Running Check()\n"); Check(); break; case POOL_STATUS_SUCCESS: LogPrint("obfuscation", "CObfuscationPool::CheckTimeout() -- Pool success -- Running Check()\n"); Check(); break; } } // check Obfuscation queue objects for timeouts int c = 0; vector::iterator it = vecObfuscationQueue.begin(); while (it != vecObfuscationQueue.end()) { if ((*it).IsExpired()) { LogPrint("obfuscation", "CObfuscationPool::CheckTimeout() : Removing expired queue entry - %d\n", c); it = vecObfuscationQueue.erase(it); } else ++it; c++; } int addLagTime = 0; if (!fMasterNode) addLagTime = 10000; //if we're the client, give the server a few extra seconds before resetting. if (state == POOL_STATUS_ACCEPTING_ENTRIES || state == POOL_STATUS_QUEUE) { c = 0; // check for a timeout and reset if needed vector::iterator it2 = entries.begin(); while (it2 != entries.end()) { if ((*it2).IsExpired()) { LogPrint("obfuscation", "CObfuscationPool::CheckTimeout() : Removing expired entry - %d\n", c); it2 = entries.erase(it2); if (entries.size() == 0) { UnlockCoins(); SetNull(); } if (fMasterNode) { RelayStatus(sessionID, GetState(), GetEntriesCount(), MASTERNODE_RESET); } } else ++it2; c++; } if (GetTimeMillis() - lastTimeChanged >= (OBFUSCATION_QUEUE_TIMEOUT * 1000) + addLagTime) { UnlockCoins(); SetNull(); } } else if (GetTimeMillis() - lastTimeChanged >= (OBFUSCATION_QUEUE_TIMEOUT * 1000) + addLagTime) { LogPrint("obfuscation", "CObfuscationPool::CheckTimeout() -- Session timed out (%ds) -- resetting\n", OBFUSCATION_QUEUE_TIMEOUT); UnlockCoins(); SetNull(); UpdateState(POOL_STATUS_ERROR); lastMessage = _("Session timed out."); } if (state == POOL_STATUS_SIGNING && GetTimeMillis() - lastTimeChanged >= (OBFUSCATION_SIGNING_TIMEOUT * 1000) + addLagTime) { LogPrint("obfuscation", "CObfuscationPool::CheckTimeout() -- Session timed out (%ds) -- restting\n", OBFUSCATION_SIGNING_TIMEOUT); ChargeFees(); UnlockCoins(); SetNull(); UpdateState(POOL_STATUS_ERROR); lastMessage = _("Signing timed out."); } } // // Check for complete queue // void CObfuscationPool::CheckForCompleteQueue() { if (!fEnableZeromint && !fMasterNode) return; /* Check to see if we're ready for submissions from clients */ // // After receiving multiple dsa messages, the queue will switch to "accepting entries" // which is the active state right before merging the transaction // if (state == POOL_STATUS_QUEUE && sessionUsers == GetMaxPoolTransactions()) { UpdateState(POOL_STATUS_ACCEPTING_ENTRIES); CObfuscationQueue dsq; dsq.nDenom = sessionDenom; dsq.vin = activeMasternode.vin; dsq.time = GetTime(); dsq.ready = true; dsq.Sign(); dsq.Relay(); } } // Check to make sure everything is signed bool CObfuscationPool::SignaturesComplete() { for (const CObfuScationEntry& v : entries) { for (const CTxDSIn& s : v.sev) { if (!s.fHasSig) return false; } } return true; } void CObfuscationPool::NewBlock() { LogPrint("obfuscation", "CObfuscationPool::NewBlock \n"); //we we're processing lots of blocks, we'll just leave if (GetTime() - lastNewBlock < 10) return; lastNewBlock = GetTime(); obfuScationPool.CheckTimeout(); } bool CObfuScationSigner::IsVinAssociatedWithPubkey(CTxIn& vin, CPubKey& pubkey) { CScript payee2; payee2 = GetScriptForDestination(pubkey.GetID()); CTransaction txVin; uint256 hash; if (GetTransaction(vin.prevout.hash, txVin, hash, true)) { for (CTxOut out : txVin.vout) { if (out.nValue == 10000 * COIN) { if (out.scriptPubKey == payee2) return true; } } } return false; } bool CObfuScationSigner::SetKey(std::string strSecret, std::string& errorMessage, CKey& key, CPubKey& pubkey) { CBitcoinSecret vchSecret; bool fGood = vchSecret.SetString(strSecret); if (!fGood) { errorMessage = _("Invalid private key."); return false; } key = vchSecret.GetKey(); pubkey = key.GetPubKey(); return true; } bool CObfuScationSigner::GetKeysFromSecret(std::string strSecret, CKey& keyRet, CPubKey& pubkeyRet) { CBitcoinSecret vchSecret; if (!vchSecret.SetString(strSecret)) return false; keyRet = vchSecret.GetKey(); pubkeyRet = keyRet.GetPubKey(); return true; } bool CObfuScationSigner::SignMessage(std::string strMessage, std::string& errorMessage, vector& vchSig, CKey key) { CHashWriter ss(SER_GETHASH, 0); ss << strMessageMagic; ss << strMessage; if (!key.SignCompact(ss.GetHash(), vchSig)) { errorMessage = _("Signing failed."); return false; } return true; } bool CObfuScationSigner::VerifyMessage(CPubKey pubkey, vector& vchSig, std::string strMessage, std::string& errorMessage) { CHashWriter ss(SER_GETHASH, 0); ss << strMessageMagic; ss << strMessage; CPubKey pubkey2; if (!pubkey2.RecoverCompact(ss.GetHash(), vchSig)) { errorMessage = _("Error recovering public key."); return false; } if (fDebug && pubkey2.GetID() != pubkey.GetID()) LogPrintf("CObfuScationSigner::VerifyMessage -- keys don't match: %s %s\n", pubkey2.GetID().ToString(), pubkey.GetID().ToString()); return (pubkey2.GetID() == pubkey.GetID()); } bool CObfuscationQueue::Sign() { if (!fMasterNode) return false; std::string strMessage = vin.ToString() + std::to_string(nDenom) + std::to_string(time) + std::to_string(ready); CKey key2; CPubKey pubkey2; std::string errorMessage = ""; if (!obfuScationSigner.SetKey(strMasterNodePrivKey, errorMessage, key2, pubkey2)) { LogPrintf("CObfuscationQueue():Relay - ERROR: Invalid Masternodeprivkey: '%s'\n", errorMessage); return false; } if (!obfuScationSigner.SignMessage(strMessage, errorMessage, vchSig, key2)) { LogPrintf("CObfuscationQueue():Relay - Sign message failed"); return false; } if (!obfuScationSigner.VerifyMessage(pubkey2, vchSig, strMessage, errorMessage)) { LogPrintf("CObfuscationQueue():Relay - Verify message failed"); return false; } return true; } bool CObfuscationQueue::Relay() { LOCK(cs_vNodes); for (CNode* pnode : vNodes) { // always relay to everyone pnode->PushMessage("dsq", (*this)); } return true; } void CObfuscationPool::RelayFinalTransaction(const int sessionID, const CTransaction& txNew) { LOCK(cs_vNodes); for (CNode* pnode : vNodes) { pnode->PushMessage("dsf", sessionID, txNew); } } void CObfuscationPool::RelayStatus(const int sessionID, const int newState, const int newEntriesCount, const int newAccepted, const int errorID) { LOCK(cs_vNodes); for (CNode* pnode : vNodes) pnode->PushMessage("dssu", sessionID, newState, newEntriesCount, newAccepted, errorID); } void CObfuscationPool::RelayCompletedTransaction(const int sessionID, const bool error, const int errorID) { LOCK(cs_vNodes); for (CNode* pnode : vNodes) pnode->PushMessage("dsc", sessionID, error, errorID); } //TODO: Rename/move to core void ThreadCheckObfuScationPool() { if (fLiteMode) return; //disable all Obfuscation/Masternode related functionality // Make this thread recognisable as the wallet flushing thread RenameThread("agrarian-obfuscation"); unsigned int c = 0; while (true) { MilliSleep(1000); //LogPrintf("ThreadCheckObfuScationPool::check timeout\n"); // try to sync from all available nodes, one step at a time masternodeSync.Process(); if (masternodeSync.IsBlockchainSynced()) { c++; // check if we should activate or ping every few minutes, // start right after sync is considered to be done if (c % MASTERNODE_PING_SECONDS == 1) activeMasternode.ManageStatus(); if (c % 60 == 0) { mnodeman.CheckAndRemove(); mnodeman.ProcessMasternodeConnections(); masternodePayments.CleanPaymentList(); CleanTransactionLocksList(); } //if(c % MASTERNODES_DUMP_SECONDS == 0) DumpMasternodes(); obfuScationPool.CheckTimeout(); obfuScationPool.CheckForCompleteQueue(); } } }