From 60409df8229fa56ae6de6fed1fa7f9c3a3e6757e Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Sun, 29 Jan 2017 12:22:14 +0400 Subject: [PATCH] InstantSend overhaul (#1288) * Multi-quorum InstantSend, complete refactoring + cleanup for IS and partial protobump * more changes: - allow InstantSend tx to have 10 inputs max - store many unique tx hashes in mapVotedOutpoints - more checks in AcceptToMemoryPoolWorker (moved from ProcessMessage + CTxLockRequest(tx).IsValid() ) * More changes: - let multiple lock candidates compete for votes - fail to vote on the same outpoint twice early * More changes: - notify CInstantSend on UpdatedBlockTip -> remove cs_main from CheckAndRemove() - notify CInstantSend on SyncTransaction -> count expiration block starting from the block corresponding tx was confirmed instead of the block lock candidate/vote was created - fixed few locks * add comments about nConfirmedHeight * Fix "Block vs Lock" edge case * Fix "Block vs Lock" edge case, p2 * Fix issues: - fix logic for locking inputs and notifying - see UpdateLockedTransaction, TryToFinalizeLockCandidate - add missing hash inserting in ProcessTxLockVote - add nMaxBlocks param to ResolveConflicts to limit max depth allowed to disconnect blocks recursively - fix false positive mempool conflict - add missing mutex locks - fix fRequireUnspent logic in CTxLockRequest::IsValid --- src/chainparams.cpp | 3 - src/consensus/params.h | 1 - src/darksend.cpp | 2 +- src/dsnotificationinterface.cpp | 7 + src/dsnotificationinterface.h | 1 + src/instantx.cpp | 1263 +++++++++++++++++++++---------- src/instantx.h | 229 ++++-- src/main.cpp | 128 +++- src/main.h | 1 + src/net.cpp | 7 +- src/qt/coincontroldialog.cpp | 6 +- src/qt/transactiondesc.cpp | 11 +- src/qt/walletmodel.cpp | 15 +- src/rpcrawtransaction.cpp | 8 +- src/version.h | 2 +- src/wallet/wallet.cpp | 41 +- 16 files changed, 1190 insertions(+), 535 deletions(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 563d66303..d6fd18bb9 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -77,7 +77,6 @@ public: consensus.nMasternodePaymentsIncreaseBlock = 158000; // actual historical value consensus.nMasternodePaymentsIncreasePeriod = 576*30; // 17280 - actual historical value consensus.nInstantSendKeepLock = 24; - consensus.nInstantSendReprocessBlocks = 15; consensus.nBudgetPaymentsStartBlock = 328008; // actual historical value consensus.nBudgetPaymentsCycleBlocks = 16616; // ~(60*24*30)/2.6, actual number of blocks per month is 200700 / 12 = 16725 consensus.nBudgetPaymentsWindowBlocks = 100; @@ -201,7 +200,6 @@ public: consensus.nMasternodePaymentsIncreaseBlock = 46000; consensus.nMasternodePaymentsIncreasePeriod = 576; consensus.nInstantSendKeepLock = 6; - consensus.nInstantSendReprocessBlocks = 4; consensus.nBudgetPaymentsStartBlock = 60000; consensus.nBudgetPaymentsCycleBlocks = 50; consensus.nBudgetPaymentsWindowBlocks = 10; @@ -308,7 +306,6 @@ public: consensus.nMasternodePaymentsIncreaseBlock = 350; consensus.nMasternodePaymentsIncreasePeriod = 10; consensus.nInstantSendKeepLock = 6; - consensus.nInstantSendReprocessBlocks = 4; consensus.nBudgetPaymentsStartBlock = 1000; consensus.nBudgetPaymentsCycleBlocks = 50; consensus.nBudgetPaymentsWindowBlocks = 10; diff --git a/src/consensus/params.h b/src/consensus/params.h index f67a42170..23fa8f301 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -41,7 +41,6 @@ struct Params { int nMasternodePaymentsIncreaseBlock; int nMasternodePaymentsIncreasePeriod; // in blocks int nInstantSendKeepLock; // in blocks - int nInstantSendReprocessBlocks; int nBudgetPaymentsStartBlock; int nBudgetPaymentsCycleBlocks; int nBudgetPaymentsWindowBlocks; diff --git a/src/darksend.cpp b/src/darksend.cpp index 032793026..9cd439ddc 100644 --- a/src/darksend.cpp +++ b/src/darksend.cpp @@ -2490,7 +2490,7 @@ void ThreadCheckDarkSendPool() mnodeman.ProcessMasternodeConnections(); mnodeman.CheckAndRemove(); mnpayments.CheckAndRemove(); - CleanTxLockCandidates(); + instantsend.CheckAndRemove(); } darkSendPool.CheckTimeout(); diff --git a/src/dsnotificationinterface.cpp b/src/dsnotificationinterface.cpp index e4c4b2f16..c2f719a0e 100644 --- a/src/dsnotificationinterface.cpp +++ b/src/dsnotificationinterface.cpp @@ -4,6 +4,7 @@ #include "dsnotificationinterface.h" #include "darksend.h" +#include "instantx.h" #include "governance.h" #include "masternodeman.h" #include "masternode-payments.h" @@ -21,7 +22,13 @@ void CDSNotificationInterface::UpdatedBlockTip(const CBlockIndex *pindex) { mnodeman.UpdatedBlockTip(pindex); darkSendPool.UpdatedBlockTip(pindex); + instantsend.UpdatedBlockTip(pindex); mnpayments.UpdatedBlockTip(pindex); governance.UpdatedBlockTip(pindex); masternodeSync.UpdatedBlockTip(pindex); } + +void CDSNotificationInterface::SyncTransaction(const CTransaction &tx, const CBlock *pblock) +{ + instantsend.SyncTransaction(tx, pblock); +} \ No newline at end of file diff --git a/src/dsnotificationinterface.h b/src/dsnotificationinterface.h index abc0d501b..68b40dd26 100644 --- a/src/dsnotificationinterface.h +++ b/src/dsnotificationinterface.h @@ -17,6 +17,7 @@ public: protected: // CValidationInterface void UpdatedBlockTip(const CBlockIndex *pindex); + void SyncTransaction(const CTransaction &tx, const CBlock *pblock); private: }; diff --git a/src/instantx.cpp b/src/instantx.cpp index fcf0dd50d..247088cd8 100644 --- a/src/instantx.cpp +++ b/src/instantx.cpp @@ -2,49 +2,45 @@ // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. - #include "activemasternode.h" #include "darksend.h" #include "instantx.h" #include "key.h" +#include "main.h" #include "masternode-sync.h" #include "masternodeman.h" #include "net.h" #include "protocol.h" #include "spork.h" #include "sync.h" +#include "txmempool.h" #include "util.h" #include "consensus/validation.h" #include -#include #include extern CWallet* pwalletMain; +extern CTxMemPool mempool; bool fEnableInstantSend = true; int nInstantSendDepth = DEFAULT_INSTANTSEND_DEPTH; int nCompleteTXLocks; -std::map mapLockRequestAccepted; -std::map mapLockRequestRejected; -std::map mapTxLockVotes; -std::map mapTxLockVotesOrphan; -std::map mapLockedInputs; - -std::map mapTxLockCandidates; -std::map mapMasternodeOrphanVotes; //track masternodes who voted with no txreq (for DOS protection) - -CCriticalSection cs_instantsend; +CInstantSend instantsend; // Transaction Locks // // step 1) Some node announces intention to lock transaction inputs via "txlreg" message -// step 2) Top INSTANTSEND_SIGNATURES_TOTAL masternodes push "txvote" message -// step 3) Once there are INSTANTSEND_SIGNATURES_REQUIRED valid "txvote" messages -// for a corresponding "txlreg" message, all inputs from that tx are treated as locked +// step 2) Top COutPointLock::SIGNATURES_TOTAL masternodes per each spent outpoint push "txvote" message +// step 3) Once there are COutPointLock::SIGNATURES_REQUIRED valid "txvote" messages per each spent outpoint +// for a corresponding "txlreg" message, all outpoints from that tx are treated as locked -void ProcessMessageInstantSend(CNode* pfrom, std::string& strCommand, CDataStream& vRecv) +// +// CInstantSend +// + +void CInstantSend::ProcessMessage(CNode* pfrom, std::string& strCommand, CDataStream& vRecv) { if(fLiteMode) return; // disable all Dash specific functionality if(!sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED)) return; @@ -56,12 +52,17 @@ void ProcessMessageInstantSend(CNode* pfrom, std::string& strCommand, CDataStrea if (strCommand == NetMsgType::TXLOCKVOTE) // InstantSend Transaction Lock Consensus Votes { + if(pfrom->nVersion < MIN_INSTANTSEND_PROTO_VERSION) return; + CTxLockVote vote; vRecv >> vote; LOCK2(cs_main, cs_instantsend); - if(mapTxLockVotes.count(vote.GetHash())) return; - mapTxLockVotes.insert(std::make_pair(vote.GetHash(), vote)); + + uint256 nVoteHash = vote.GetHash(); + + if(mapTxLockVotes.count(nVoteHash)) return; + mapTxLockVotes.insert(std::make_pair(nVoteHash, vote)); ProcessTxLockVote(pfrom, vote); @@ -69,291 +70,292 @@ void ProcessMessageInstantSend(CNode* pfrom, std::string& strCommand, CDataStrea } } -bool IsInstantSendTxValid(const CTransaction& txCandidate) +bool CInstantSend::ProcessTxLockRequest(const CTxLockRequest& txLockRequest) { - if(txCandidate.vout.size() < 1) return false; - - { - LOCK(cs_main); - if(!CheckFinalTx(txCandidate)) { - LogPrint("instantsend", "IsInstantSendTxValid -- Transaction is not final: txCandidate=%s", txCandidate.ToString()); - return false; - } - } - - int64_t nValueIn = 0; - int64_t nValueOut = 0; - bool fMissingInputs = false; - - BOOST_FOREACH(const CTxOut& txout, txCandidate.vout) { - // InstandSend supports normal scripts and unspendable (i.e. data) scripts. - // TODO: Look into other script types that are normal and can be included - if(!txout.scriptPubKey.IsNormalPaymentScript() && !txout.scriptPubKey.IsUnspendable()) { - LogPrint("instantsend", "IsInstantSendTxValid -- Invalid Script %s", txCandidate.ToString()); - return false; - } - nValueOut += txout.nValue; - } - - BOOST_FOREACH(const CTxIn& txin, txCandidate.vin) { - CTransaction tx2; - uint256 hash; - if(GetTransaction(txin.prevout.hash, tx2, Params().GetConsensus(), hash, true)) { - if(tx2.vout.size() > txin.prevout.n) - nValueIn += tx2.vout[txin.prevout.n].nValue; - } else { - fMissingInputs = true; - } - } - - if(nValueOut > sporkManager.GetSporkValue(SPORK_5_INSTANTSEND_MAX_VALUE)*COIN) { - LogPrint("instantsend", "IsInstantSendTxValid -- Transaction value too high: nValueOut=%d, txCandidate=%s", nValueOut, txCandidate.ToString()); - return false; - } - - if(fMissingInputs) { - LogPrint("instantsend", "IsInstantSendTxValid -- Unknown inputs in transaction: txCandidate=%s", txCandidate.ToString()); - /* - This happens sometimes for an unknown reason, so we'll return that it's a valid transaction. - If someone submits an invalid transaction it will be rejected by the network anyway and this isn't - very common, but we don't want to block IX just because the client can't figure out the fee. - */ - return true; - } - - if(nValueIn - nValueOut < INSTANTSEND_MIN_FEE) { - LogPrint("instantsend", "IsInstantSendTxValid -- did not include enough fees in transaction: fees=%d, txCandidate=%s", nValueOut - nValueIn, txCandidate.ToString()); - return false; - } - - return true; -} - -bool ProcessTxLockRequest(CNode* pfrom, const CTransaction &tx) -{ - if(!IsInstantSendTxValid(tx)) return false; - - int nBlockHeight = CreateTxLockCandidate(tx); - if(!nBlockHeight) { - // smth is not right - return false; - } - LOCK2(cs_main, cs_instantsend); - uint256 txHash = tx.GetHash(); - mapLockRequestAccepted.insert(std::make_pair(txHash, tx)); - LogPrintf("TXLOCKREQUEST -- Transaction Lock Request: %s %s : accepted %s\n", - pfrom ? pfrom->addr.ToString() : "", pfrom ? pfrom->cleanSubVer : "", txHash.ToString()); + uint256 txHash = txLockRequest.GetHash(); - CreateTxLockVote(tx, nBlockHeight); + // Check to see if we conflict with existing completed lock, + // fail if so, there can't be 2 completed locks for the same outpoint + BOOST_FOREACH(const CTxIn& txin, txLockRequest.vin) { + std::map::iterator it = mapLockedOutpoints.find(txin.prevout); + if(it != mapLockedOutpoints.end()) { + // Conflicting with complete lock, ignore this one + // (this could be the one we have but we don't want to try to lock it twice anyway) + LogPrintf("CInstantSend::ProcessTxLockRequest -- WARNING: Found conflicting completed Transaction Lock, skipping current one, txid=%s, completed lock txid=%s\n", + txLockRequest.GetHash().ToString(), it->second.ToString()); + return false; + } + } + + // Check to see if there are votes for conflicting request, + // if so - do not fail, just warn user + BOOST_FOREACH(const CTxIn& txin, txLockRequest.vin) { + std::map >::iterator it = mapVotedOutpoints.find(txin.prevout); + if(it != mapVotedOutpoints.end()) { + BOOST_FOREACH(const uint256& hash, it->second) { + if(hash != txLockRequest.GetHash()) { + LogPrint("instantsend", "CInstantSend::ProcessTxLockRequest -- Double spend attempt! %s\n", txin.prevout.ToStringShort()); + // do not fail here, let it go and see which one will get the votes to be locked + } + } + } + } + + if(!CreateTxLockCandidate(txLockRequest)) { + // smth is not right + LogPrintf("CInstantSend::ProcessTxLockRequest -- CreateTxLockCandidate failed, txid=%s\n", txHash.ToString()); + return false; + } + LogPrintf("CInstantSend::ProcessTxLockRequest -- accepted, txid=%s\n", txHash.ToString()); + + std::map::iterator itLockCandidate = mapTxLockCandidates.find(txHash); + CTxLockCandidate& txLockCandidate = itLockCandidate->second; + Vote(txLockCandidate); ProcessOrphanTxLockVotes(); // Masternodes will sometimes propagate votes before the transaction is known to the client. - // If this just happened - update transaction status, try forcing external script notification, - // lock inputs and resolve conflicting locks - if(IsLockedInstandSendTransaction(txHash)) { - UpdateLockedTransaction(tx, true); - LockTransactionInputs(tx); - ResolveConflicts(tx); + // If this just happened - lock inputs, resolve conflicting locks, update transaction status + // forcing external script notification. + TryToFinalizeLockCandidate(txLockCandidate); + + return true; +} + +bool CInstantSend::CreateTxLockCandidate(const CTxLockRequest& txLockRequest) +{ + // Normally we should require all outpoints to be unspent, but in case we are reprocessing + // because of a lot of legit orphan votes we should also check already spent outpoints. + uint256 txHash = txLockRequest.GetHash(); + if(!txLockRequest.IsValid(!IsEnoughOrphanVotesForTx(txLockRequest))) return false; + + LOCK(cs_instantsend); + + std::map::iterator itLockCandidate = mapTxLockCandidates.find(txHash); + if(itLockCandidate == mapTxLockCandidates.end()) { + LogPrintf("CInstantSend::CreateTxLockCandidate -- new, txid=%s\n", txHash.ToString()); + + CTxLockCandidate txLockCandidate(txLockRequest); + // all inputs should already be checked by txLockRequest.IsValid() above, just use them now + BOOST_REVERSE_FOREACH(const CTxIn& txin, txLockRequest.vin) { + txLockCandidate.AddOutPointLock(txin.prevout); + } + mapTxLockCandidates.insert(std::make_pair(txHash, txLockCandidate)); + } else { + LogPrint("instantsend", "CInstantSend::CreateTxLockCandidate -- seen, txid=%s\n", txHash.ToString()); } return true; } -int64_t CreateTxLockCandidate(const CTransaction& tx) -{ - // Find the age of the first input but all inputs must be old enough too - int64_t nTxAge = 0; - BOOST_REVERSE_FOREACH(const CTxIn& txin, tx.vin) { - nTxAge = GetInputAge(txin); - if(nTxAge < 5) { //1 less than the "send IX" gui requires, incase of a block propagating the network at the time - LogPrintf("CreateTxLockCandidate -- Transaction not found / too new: nTxAge=%d, txid=%s\n", nTxAge, tx.GetHash().ToString()); - return 0; - } - } - - /* - Use a blockheight newer than the input. - This prevents attackers from using transaction mallibility to predict which masternodes - they'll use. - */ - int nCurrentHeight = 0; - int nLockInputHeight = 0; - { - LOCK(cs_main); - if(!chainActive.Tip()) return 0; - nCurrentHeight = chainActive.Height(); - nLockInputHeight = nCurrentHeight - nTxAge + 4; - } - - uint256 txHash = tx.GetHash(); - - LOCK(cs_instantsend); - if(!mapTxLockCandidates.count(txHash)) { - LogPrintf("CreateTxLockCandidate -- New Transaction Lock Candidate! txid=%s\n", txHash.ToString()); - - CTxLockCandidate txLockCandidate; - txLockCandidate.nBlockHeight = nLockInputHeight; - //locks expire after nInstantSendKeepLock confirmations - txLockCandidate.nExpirationBlock = nCurrentHeight + Params().GetConsensus().nInstantSendKeepLock; - txLockCandidate.nTimeout = GetTime()+(60*5); - txLockCandidate.txHash = txHash; - mapTxLockCandidates.insert(std::make_pair(txHash, txLockCandidate)); - } else { - mapTxLockCandidates[txHash].nBlockHeight = nLockInputHeight; - LogPrint("instantsend", "CreateTxLockCandidate -- Transaction Lock Candidate exists! txid=%s\n", txHash.ToString()); - } - - return nLockInputHeight; -} - -// check if we need to vote on this transaction -void CreateTxLockVote(const CTransaction& tx, int nBlockHeight) +void CInstantSend::Vote(CTxLockCandidate& txLockCandidate) { if(!fMasterNode) return; - int n = mnodeman.GetMasternodeRank(activeMasternode.vin, nBlockHeight, MIN_INSTANTSEND_PROTO_VERSION); + LOCK2(cs_main, cs_instantsend); - if(n == -1) { - LogPrint("instantsend", "CreateTxLockVote -- Unknown Masternode %s\n", activeMasternode.vin.prevout.ToStringShort()); - return; + uint256 txHash = txLockCandidate.GetHash(); + // check if we need to vote on this candidate's outpoints, + // it's possible that we need to vote for several of them + std::map::iterator itOutpointLock = txLockCandidate.mapOutPointLocks.begin(); + while(itOutpointLock != txLockCandidate.mapOutPointLocks.end()) { + + int nPrevoutHeight = GetUTXOHeight(itOutpointLock->first); + if(nPrevoutHeight == -1) { + LogPrint("instantsend", "CInstantSend::Vote -- Failed to find UTXO %s\n", itOutpointLock->first.ToStringShort()); + return; + } + + int nLockInputHeight = nPrevoutHeight + 4; + + int n = mnodeman.GetMasternodeRank(activeMasternode.vin, nLockInputHeight, MIN_INSTANTSEND_PROTO_VERSION); + + if(n == -1) { + LogPrint("instantsend", "CInstantSend::Vote -- Unknown Masternode %s\n", activeMasternode.vin.prevout.ToStringShort()); + continue; + } + + int nSignaturesTotal = COutPointLock::SIGNATURES_TOTAL; + if(n > nSignaturesTotal) { + LogPrint("instantsend", "CInstantSend::Vote -- Masternode not in the top %d (%d)\n", nSignaturesTotal, n); + continue; + } + + LogPrint("instantsend", "CInstantSend::Vote -- In the top %d (%d)\n", nSignaturesTotal, n); + + std::map >::iterator itVoted = mapVotedOutpoints.find(itOutpointLock->first); + + // Check to see if we already voted for this outpoint, + // refuse to vote twice or to include the same outpoint in another tx + bool fAlreadyVoted = false; + if(itVoted != mapVotedOutpoints.end()) { + BOOST_FOREACH(const uint256& hash, itVoted->second) { + std::map::iterator it2 = mapTxLockCandidates.find(hash); + if(it2->second.HasMasternodeVoted(itOutpointLock->first, activeMasternode.vin.prevout)) { + // we already voted for this outpoint to be included either in the same tx or in a competing one, + // skip it anyway + fAlreadyVoted = true; + LogPrintf("CInstantSend::Vote -- WARNING: We already voted for this outpoint, skipping: txHash=%s, outpoint=%s\n", + txHash.ToString(), itOutpointLock->first.ToStringShort()); + break; + } + } + } + if(fAlreadyVoted) continue; // skip to the next outpoint + + // we haven't voted for this outpoint yet, let's try to do this now + CTxLockVote vote(txHash, itOutpointLock->first, activeMasternode.vin.prevout); + + if(!vote.Sign()) { + LogPrintf("CInstantSend::Vote -- Failed to sign consensus vote\n"); + return; + } + if(!vote.CheckSignature()) { + LogPrintf("CInstantSend::Vote -- Signature invalid\n"); + return; + } + + // vote constructed sucessfully, let's store and relay it + uint256 nVoteHash = vote.GetHash(); + mapTxLockVotes.insert(std::make_pair(nVoteHash, vote)); + if(itOutpointLock->second.AddVote(vote)) { + LogPrintf("CInstantSend::Vote -- Vote created successfully, relaying: txHash=%s, outpoint=%s, vote=%s\n", + txHash.ToString(), itOutpointLock->first.ToStringShort(), nVoteHash.ToString()); + + if(itVoted == mapVotedOutpoints.end()) { + std::set setHashes; + setHashes.insert(txHash); + mapVotedOutpoints.insert(std::make_pair(itOutpointLock->first, setHashes)); + } else { + mapVotedOutpoints[itOutpointLock->first].insert(txHash); + if(mapVotedOutpoints[itOutpointLock->first].size() > 1) { + // it's ok to continue, just warn user + LogPrintf("CInstantSend::Vote -- WARNING: Vote conflicts with some existing votes: txHash=%s, outpoint=%s, vote=%s\n", + txHash.ToString(), itOutpointLock->first.ToStringShort(), nVoteHash.ToString()); + } + } + + vote.Relay(); + } + + ++itOutpointLock; } - - if(n > INSTANTSEND_SIGNATURES_TOTAL) { - LogPrint("instantsend", "CreateTxLockVote -- Masternode not in the top %d (%d)\n", INSTANTSEND_SIGNATURES_TOTAL, n); - return; - } - /* - nBlockHeight calculated from the transaction is the authoritive source - */ - - LogPrint("instantsend", "CreateTxLockVote -- In the top %d (%d)\n", INSTANTSEND_SIGNATURES_TOTAL, n); - - CTxLockVote vote; - vote.vinMasternode = activeMasternode.vin; - vote.txHash = tx.GetHash(); - vote.nBlockHeight = nBlockHeight; - if(!vote.Sign()) { - LogPrintf("CreateTxLockVote -- Failed to sign consensus vote\n"); - return; - } - if(!vote.CheckSignature()) { - LogPrintf("CreateTxLockVote -- Signature invalid\n"); - return; - } - - { - LOCK(cs_instantsend); - mapTxLockVotes[vote.GetHash()] = vote; - } - - CInv inv(MSG_TXLOCK_VOTE, vote.GetHash()); - RelayInv(inv); } //received a consensus vote -bool ProcessTxLockVote(CNode* pnode, CTxLockVote& vote) +bool CInstantSend::ProcessTxLockVote(CNode* pfrom, CTxLockVote& vote) { - LOCK(cs_instantsend); + LOCK2(cs_main, cs_instantsend); + + uint256 txHash = vote.GetTxHash(); + + if(!vote.IsValid(pfrom)) { + // could be because of missing MN + LogPrint("instantsend", "CInstantSend::ProcessTxLockVote -- Vote is invalid, txid=%s\n", txHash.ToString()); + return false; + } + // Masternodes will sometimes propagate votes before the transaction is known to the client, // will actually process only after the lock request itself has arrived - if(!mapLockRequestAccepted.count(vote.txHash)) { + + std::map::iterator it = mapTxLockCandidates.find(txHash); + if(it == mapTxLockCandidates.end()) { if(!mapTxLockVotesOrphan.count(vote.GetHash())) { - LogPrint("instantsend", "ProcessTxLockVote -- Orphan vote: txid=%s masternode=%s new\n", vote.txHash.ToString(), vote.vinMasternode.prevout.ToStringShort()); - vote.nOrphanExpireTime = GetTime() + 60; // keep orphan votes for 1 minute mapTxLockVotesOrphan[vote.GetHash()] = vote; + LogPrint("instantsend", "CInstantSend::ProcessTxLockVote -- Orphan vote: txid=%s masternode=%s new\n", + txHash.ToString(), vote.GetMasternodeOutpoint().ToStringShort()); + bool fReprocess = true; + std::map::iterator itLockRequest = mapLockRequestAccepted.find(txHash); + if(itLockRequest == mapLockRequestAccepted.end()) { + itLockRequest = mapLockRequestRejected.find(txHash); + if(itLockRequest == mapLockRequestRejected.end()) { + // still too early, wait for tx lock request + fReprocess = false; + } + } + if(fReprocess && IsEnoughOrphanVotesForTx(itLockRequest->second)) { + // We have enough votes for corresponding lock to complete, + // tx lock request should already be received at this stage. + LogPrint("instantsend", "CInstantSend::ProcessTxLockVote -- Found enough orphan votes, reprocessing Transaction Lock Request: txid=%s\n", txHash.ToString()); + ProcessTxLockRequest(itLockRequest->second); + return true; + } } else { - LogPrint("instantsend", "ProcessTxLockVote -- Orphan vote: txid=%s masternode=%s seen\n", vote.txHash.ToString(), vote.vinMasternode.prevout.ToStringShort()); + LogPrint("instantsend", "CInstantSend::ProcessTxLockVote -- Orphan vote: txid=%s masternode=%s seen\n", + txHash.ToString(), vote.GetMasternodeOutpoint().ToStringShort()); } // This tracks those messages and allows only the same rate as of the rest of the network + // TODO: make sure this works good enough for multi-quorum int nMasternodeOrphanExpireTime = GetTime() + 60*10; // keep time data for 10 minutes - if(!mapMasternodeOrphanVotes.count(vote.vinMasternode.prevout)) { - mapMasternodeOrphanVotes[vote.vinMasternode.prevout] = nMasternodeOrphanExpireTime; + if(!mapMasternodeOrphanVotes.count(vote.GetMasternodeOutpoint())) { + mapMasternodeOrphanVotes[vote.GetMasternodeOutpoint()] = nMasternodeOrphanExpireTime; } else { - int64_t nPrevOrphanVote = mapMasternodeOrphanVotes[vote.vinMasternode.prevout]; + int64_t nPrevOrphanVote = mapMasternodeOrphanVotes[vote.GetMasternodeOutpoint()]; if(nPrevOrphanVote > GetTime() && nPrevOrphanVote > GetAverageMasternodeOrphanVoteTime()) { - LogPrint("instantsend", "ProcessTxLockVote -- masternode is spamming orphan Transaction Lock Votes: txid=%s masternode=%s\n", - vote.vinMasternode.prevout.ToStringShort(), vote.txHash.ToString()); + LogPrint("instantsend", "CInstantSend::ProcessTxLockVote -- masternode is spamming orphan Transaction Lock Votes: txid=%s masternode=%s\n", + txHash.ToString(), vote.GetMasternodeOutpoint().ToStringShort()); // Misbehaving(pfrom->id, 1); return false; } // not spamming, refresh - mapMasternodeOrphanVotes[vote.vinMasternode.prevout] = nMasternodeOrphanExpireTime; + mapMasternodeOrphanVotes[vote.GetMasternodeOutpoint()] = nMasternodeOrphanExpireTime; } return true; } - LogPrint("instantsend", "ProcessTxLockVote -- Transaction Lock Vote, txid=%s\n", vote.txHash.ToString()); + LogPrint("instantsend", "CInstantSend::ProcessTxLockVote -- Transaction Lock Vote, txid=%s\n", txHash.ToString()); - if(!mnodeman.Has(vote.vinMasternode)) { - LogPrint("instantsend", "ProcessTxLockVote -- Unknown masternode %s\n", vote.vinMasternode.prevout.ToStringShort()); - return false; - } - - int n = mnodeman.GetMasternodeRank(vote.vinMasternode, vote.nBlockHeight, MIN_INSTANTSEND_PROTO_VERSION); - - if(n == -1) { - //can be caused by past versions trying to vote with an invalid protocol - LogPrint("instantsend", "ProcessTxLockVote -- Outdated masternode %s\n", vote.vinMasternode.prevout.ToStringShort()); - if(pnode) { - mnodeman.AskForMN(pnode, vote.vinMasternode); + std::map >::iterator it1 = mapVotedOutpoints.find(vote.GetOutpoint()); + if(it1 != mapVotedOutpoints.end()) { + BOOST_FOREACH(const uint256& hash, it1->second) { + if(hash != txHash) { + // same outpoint was already voted to be locked by another tx lock request, + // find out if the same mn voted on this outpoint before + std::map::iterator it2 = mapTxLockCandidates.find(hash); + if(it2->second.HasMasternodeVoted(vote.GetOutpoint(), vote.GetMasternodeOutpoint())) { + // yes, it did, refuse to accept a vote to include the same outpoint in another tx + // from the same masternode. + // TODO: apply pose ban score to this masternode? + // NOTE: if we decide to apply pose ban score here, this vote must be relayed further + // to let all other nodes know about this node's misbehaviour and let them apply + // pose ban score too. + LogPrintf("CInstantSend::ProcessTxLockVote -- masternode sent conflicting votes! %s\n", vote.GetMasternodeOutpoint().ToStringShort()); + return false; + } + } } - return false; - } - LogPrint("instantsend", "ProcessTxLockVote -- Masternode %s, rank=%d\n", vote.vinMasternode.prevout.ToStringShort(), n); - - if(n > INSTANTSEND_SIGNATURES_TOTAL) { - LogPrint("instantsend", "ProcessTxLockVote -- Masternode %s is not in the top %d (%d), vote hash=%s\n", - vote.vinMasternode.prevout.ToStringShort(), INSTANTSEND_SIGNATURES_TOTAL, n, vote.GetHash().ToString()); - return false; + // we have votes by other masternodes only (so far), let's continue and see who will win + it1->second.insert(txHash); + } else { + std::set setHashes; + setHashes.insert(txHash); + mapVotedOutpoints.insert(std::make_pair(vote.GetOutpoint(), setHashes)); } - if(!vote.CheckSignature()) { - LogPrintf("ProcessTxLockVote -- Signature invalid\n"); - // don't ban, it could just be a non-synced masternode - if(pnode) { - mnodeman.AskForMN(pnode, vote.vinMasternode); - } - return false; - } + CTxLockCandidate& txLockCandidate = it->second; - if(!mapTxLockCandidates.count(vote.txHash)) { + if(!txLockCandidate.AddVote(vote)) { // this should never happen return false; } - //compile consessus vote - mapTxLockCandidates[vote.txHash].AddVote(vote); + int nSignatures = txLockCandidate.CountVotes(); + int nSignaturesMax = txLockCandidate.txLockRequest.GetMaxSignatures(); + LogPrint("instantsend", "CInstantSend::ProcessTxLockVote -- Transaction Lock signatures count: %d/%d, vote hash=%s\n", + nSignatures, nSignaturesMax, vote.GetHash().ToString()); - int nSignatures = mapTxLockCandidates[vote.txHash].CountVotes(); - LogPrint("instantsend", "ProcessTxLockVote -- Transaction Lock signatures count: %d, vote hash=%s\n", nSignatures, vote.GetHash().ToString()); + TryToFinalizeLockCandidate(txLockCandidate); - if(nSignatures >= INSTANTSEND_SIGNATURES_REQUIRED) { - LogPrint("instantsend", "ProcessTxLockVote -- Transaction Lock Is Complete! txid=%s\n", vote.txHash.ToString()); - - if(!FindConflictingLocks(mapLockRequestAccepted[vote.txHash])) { //????? - if(mapLockRequestAccepted.count(vote.txHash)) { - UpdateLockedTransaction(mapLockRequestAccepted[vote.txHash]); - LockTransactionInputs(mapLockRequestAccepted[vote.txHash]); - } else if(mapLockRequestRejected.count(vote.txHash)) { - ResolveConflicts(mapLockRequestRejected[vote.txHash]); ///????? - } else { - LogPrint("instantsend", "ProcessTxLockVote -- Transaction Lock is missing! nSignatures=%d, vote hash=%s\n", nSignatures, vote.GetHash().ToString()); - } - } - } - - CInv inv(MSG_TXLOCK_VOTE, vote.GetHash()); - RelayInv(inv); + vote.Relay(); return true; } -void ProcessOrphanTxLockVotes() +void CInstantSend::ProcessOrphanTxLockVotes() { LOCK2(cs_main, cs_instantsend); std::map::iterator it = mapTxLockVotesOrphan.begin(); @@ -366,92 +368,192 @@ void ProcessOrphanTxLockVotes() } } -void UpdateLockedTransaction(const CTransaction& tx, bool fForceNotification) +bool CInstantSend::IsEnoughOrphanVotesForTx(const CTxLockRequest& txLockRequest) +{ + // There could be a situation when we already have quite a lot of votes + // but tx lock request still wasn't received. Let's scan through + // orphan votes to check if this is the case. + BOOST_FOREACH(const CTxIn& txin, txLockRequest.vin) { + if(!IsEnoughOrphanVotesForTxAndOutPoint(txLockRequest.GetHash(), txin.prevout)) { + return false; + } + } + return true; +} + +bool CInstantSend::IsEnoughOrphanVotesForTxAndOutPoint(const uint256& txHash, const COutPoint& outpoint) +{ + // Scan orphan votes to check if this outpoint has enough orphan votes to be locked in some tx. + LOCK2(cs_main, cs_instantsend); + int nCountVotes = 0; + std::map::iterator it = mapTxLockVotesOrphan.begin(); + while(it != mapTxLockVotesOrphan.end()) { + if(it->second.GetTxHash() == txHash && it->second.GetOutpoint() == outpoint) { + nCountVotes++; + if(nCountVotes >= COutPointLock::SIGNATURES_REQUIRED) { + return true; + } + } + ++it; + } + return false; +} + +void CInstantSend::TryToFinalizeLockCandidate(const CTxLockCandidate& txLockCandidate) +{ + LOCK2(cs_main, cs_instantsend); + + uint256 txHash = txLockCandidate.txLockRequest.GetHash(); + if(txLockCandidate.IsAllOutPointsReady() && !IsLockedInstantSendTransaction(txHash)) { + // we have enough votes now + LogPrint("instantsend", "CInstantSend::TryToFinalizeLockCandidate -- Transaction Lock is ready to complete, txid=%s\n", txHash.ToString()); + if(ResolveConflicts(txLockCandidate, Params().GetConsensus().nInstantSendKeepLock)) { + LockTransactionInputs(txLockCandidate); + UpdateLockedTransaction(txLockCandidate); + } + } +} + +void CInstantSend::UpdateLockedTransaction(const CTxLockCandidate& txLockCandidate) { LOCK(cs_instantsend); - // there should be no conflicting locks - if(FindConflictingLocks(tx)) return; - uint256 txHash = tx.GetHash(); - // there must be a successfully verified lock request - if(!mapLockRequestAccepted.count(txHash)) return; - int nSignatures = GetTransactionLockSignatures(txHash); + uint256 txHash = txLockCandidate.GetHash(); + + if(!IsLockedInstantSendTransaction(txHash)) return; // not a locked tx, do not update/notify #ifdef ENABLE_WALLET if(pwalletMain && pwalletMain->UpdatedTransaction(txHash)) { // bumping this to update UI nCompleteTXLocks++; - // a transaction lock must have enough signatures to trigger this notification - if(nSignatures == INSTANTSEND_SIGNATURES_REQUIRED || (fForceNotification && nSignatures > INSTANTSEND_SIGNATURES_REQUIRED)) { - // notify an external script once threshold is reached - std::string strCmd = GetArg("-instantsendnotify", ""); - if(!strCmd.empty()) { - boost::replace_all(strCmd, "%s", txHash.GetHex()); - boost::thread t(runCommand, strCmd); // thread runs free - } + // notify an external script once threshold is reached + std::string strCmd = GetArg("-instantsendnotify", ""); + if(!strCmd.empty()) { + boost::replace_all(strCmd, "%s", txHash.GetHex()); + boost::thread t(runCommand, strCmd); // thread runs free } } #endif - if(nSignatures == INSTANTSEND_SIGNATURES_REQUIRED || (fForceNotification && nSignatures > INSTANTSEND_SIGNATURES_REQUIRED)) - GetMainSignals().NotifyTransactionLock(tx); + GetMainSignals().NotifyTransactionLock(txLockCandidate.txLockRequest); + + LogPrint("instantsend", "CInstantSend::UpdateLockedTransaction -- done, txid=%s\n", txHash.ToString()); } -void LockTransactionInputs(const CTransaction& tx) { - LOCK(cs_instantsend); - if(!mapLockRequestAccepted.count(tx.GetHash())) return; - - BOOST_FOREACH(const CTxIn& txin, tx.vin) - if(!mapLockedInputs.count(txin.prevout)) - mapLockedInputs.insert(std::make_pair(txin.prevout, tx.GetHash())); -} - -bool FindConflictingLocks(const CTransaction& tx) +void CInstantSend::LockTransactionInputs(const CTxLockCandidate& txLockCandidate) { LOCK(cs_instantsend); - /* - It's possible (very unlikely though) to get 2 conflicting transaction locks approved by the network. - In that case, they will cancel each other out. - Blocks could have been rejected during this time, which is OK. After they cancel out, the client will - rescan the blocks and find they're acceptable and then take the chain with the most work. - */ - uint256 txHash = tx.GetHash(); - BOOST_FOREACH(const CTxIn& txin, tx.vin) { - if(mapLockedInputs.count(txin.prevout)) { - if(mapLockedInputs[txin.prevout] != txHash) { - LogPrintf("FindConflictingLocks -- found two complete conflicting Transaction Locks, removing both: txid=%s, txin=%s", txHash.ToString(), mapLockedInputs[txin.prevout].ToString()); + uint256 txHash = txLockCandidate.GetHash(); - if(mapTxLockCandidates.count(txHash)) - mapTxLockCandidates[txHash].nExpirationBlock = -1; + if(!txLockCandidate.IsAllOutPointsReady()) return; - if(mapTxLockCandidates.count(mapLockedInputs[txin.prevout])) - mapTxLockCandidates[mapLockedInputs[txin.prevout]].nExpirationBlock = -1; + std::map::const_iterator it = txLockCandidate.mapOutPointLocks.begin(); - return true; + while(it != txLockCandidate.mapOutPointLocks.end()) { + mapLockedOutpoints.insert(std::make_pair(it->first, txHash)); + ++it; + } + LogPrint("instantsend", "CInstantSend::LockTransactionInputs -- done, txid=%s\n", txHash.ToString()); +} + +bool CInstantSend::GetLockedOutPointTxHash(const COutPoint& outpoint, uint256& hashRet) +{ + LOCK(cs_instantsend); + std::map::iterator it = mapLockedOutpoints.find(outpoint); + if(it == mapLockedOutpoints.end()) return false; + hashRet = it->second; + return true; +} + +bool CInstantSend::ResolveConflicts(const CTxLockCandidate& txLockCandidate, int nMaxBlocks) +{ + if(nMaxBlocks < 1) return false; + + LOCK2(cs_main, cs_instantsend); + + uint256 txHash = txLockCandidate.GetHash(); + + // make sure the lock is ready + if(!txLockCandidate.IsAllOutPointsReady()) return true; // not an error + + LOCK(mempool.cs); // protect mempool.mapNextTx, mempool.mapTx + + bool fMempoolConflict = false; + + BOOST_FOREACH(const CTxIn& txin, txLockCandidate.txLockRequest.vin) { + uint256 hashConflicting; + if(GetLockedOutPointTxHash(txin.prevout, hashConflicting) && txHash != hashConflicting) { + // conflicting with complete lock, ignore current one + LogPrintf("CInstantSend::ResolveConflicts -- WARNING: Found conflicting completed Transaction Lock, skipping current one, txid=%s, conflicting txid=%s\n", + txHash.ToString(), hashConflicting.ToString()); + return false; // can't/shouldn't do anything + } else if (mempool.mapNextTx.count(txin.prevout)) { + // check if it's in mempool + hashConflicting = mempool.mapNextTx[txin.prevout].ptx->GetHash(); + if(txHash == hashConflicting) continue; // matches current, not a conflict, skip to next txin + // conflicting with tx in mempool + fMempoolConflict = true; + if(HasTxLockRequest(hashConflicting)) { + // There can be only one completed lock, the other lock request should never complete + LogPrintf("CInstantSend::ResolveConflicts -- WARNING: Found conflicting Transaction Lock Request, replacing by completed Transaction Lock, txid=%s, conflicting txid=%s\n", + txHash.ToString(), hashConflicting.ToString()); + } else { + // If this lock is completed, we don't really care about normal conflicting txes. + LogPrintf("CInstantSend::ResolveConflicts -- WARNING: Found conflicting transaction, replacing by completed Transaction Lock, txid=%s, conflicting txid=%s\n", + txHash.ToString(), hashConflicting.ToString()); } } + } // FOREACH + if(fMempoolConflict) { + std::list removed; + // remove every tx conflicting with current Transaction Lock Request + mempool.removeConflicts(txLockCandidate.txLockRequest, removed); + // and try to accept it in mempool again + CValidationState state; + bool fMissingInputs = false; + if(!AcceptToMemoryPool(mempool, state, txLockCandidate.txLockRequest, true, &fMissingInputs)) { + LogPrintf("CInstantSend::ResolveConflicts -- ERROR: Failed to accept completed Transaction Lock to mempool, txid=%s\n", txHash.ToString()); + return false; + } + LogPrintf("CInstantSend::ResolveConflicts -- Accepted completed Transaction Lock, txid=%s\n", txHash.ToString()); + return true; } + // No conflicts were found so far, check to see if it was already included in block + CTransaction txTmp; + uint256 hashBlock; + if(GetTransaction(txHash, txTmp, Params().GetConsensus(), hashBlock, true) && hashBlock != uint256()) { + LogPrint("instantsend", "CInstantSend::ResolveConflicts -- Done, %s is included in block %s\n", txHash.ToString(), hashBlock.ToString()); + return true; + } + // Not in block yet, make sure all its inputs are still unspent + BOOST_FOREACH(const CTxIn& txin, txLockCandidate.txLockRequest.vin) { + CCoins coins; + if(!pcoinsTip->GetCoins(txin.prevout.hash, coins) || + (unsigned int)txin.prevout.n>=coins.vout.size() || + coins.vout[txin.prevout.n].IsNull()) { + // Not in UTXO anymore? A conflicting tx was mined while we were waiting for votes. + // Reprocess tip to make sure tx for this lock is included. + LogPrintf("CTxLockRequest::ResolveConflicts -- Failed to find UTXO %s - disconnecting tip...\n", txin.prevout.ToStringShort()); + DisconnectBlocks(1); + // Recursively check at "new" old height. Conflicting tx should be rejected by AcceptToMemoryPool. + ResolveConflicts(txLockCandidate, nMaxBlocks - 1); + LogPrintf("CTxLockRequest::ResolveConflicts -- Failed to find UTXO %s - activating best chain...\n", txin.prevout.ToStringShort()); + // Activate best chain, block which includes conflicting tx should be rejected by ConnectBlock. + CValidationState state; + if(!ActivateBestChain(state, Params()) || !state.IsValid()) { + LogPrintf("CTxLockRequest::ResolveConflicts -- ActivateBestChain failed, txid=%s\n", txin.prevout.ToStringShort()); + return false; + } + LogPrintf("CTxLockRequest::ResolveConflicts -- Failed to find UTXO %s - fixed!\n", txin.prevout.ToStringShort()); + } + } + LogPrint("instantsend", "CInstantSend::ResolveConflicts -- Done, txid=%s\n", txHash.ToString()); - return false; + return true; } -void ResolveConflicts(const CTransaction& tx) -{ - LOCK(cs_instantsend); - uint256 txHash = tx.GetHash(); - // resolve conflicts - if (IsLockedInstandSendTransaction(txHash) && !FindConflictingLocks(tx)) { //????? - LogPrintf("ResolveConflicts -- Found existing complete Transaction Lock, resolving...\n"); - - //reprocess the last nInstantSendReprocessBlocks blocks - ReprocessBlocks(Params().GetConsensus().nInstantSendReprocessBlocks); - if(!mapLockRequestAccepted.count(txHash)) - mapLockRequestAccepted.insert(std::make_pair(txHash, tx)); //????? - } -} - -int64_t GetAverageMasternodeOrphanVoteTime() +int64_t CInstantSend::GetAverageMasternodeOrphanVoteTime() { LOCK(cs_instantsend); // NOTE: should never actually call this function when mapMasternodeOrphanVotes is empty @@ -468,118 +570,418 @@ int64_t GetAverageMasternodeOrphanVoteTime() return total / mapMasternodeOrphanVotes.size(); } -void CleanTxLockCandidates() +void CInstantSend::CheckAndRemove() { - int nHeight; - { - LOCK(cs_main); - nHeight = chainActive.Height(); - } + if(!pCurrentBlockIndex) return; LOCK(cs_instantsend); - std::map::iterator it = mapTxLockCandidates.begin(); + std::map::iterator itLockCandidate = mapTxLockCandidates.begin(); - while(it != mapTxLockCandidates.end()) { - CTxLockCandidate &txLockCandidate = it->second; - if(nHeight > txLockCandidate.nExpirationBlock) { - LogPrintf("CleanTxLockCandidates -- Removing expired Transaction Lock Candidate: txid=%s\n", txLockCandidate.txHash.ToString()); - - if(mapLockRequestAccepted.count(txLockCandidate.txHash)){ - CTransaction& tx = mapLockRequestAccepted[txLockCandidate.txHash]; - - BOOST_FOREACH(const CTxIn& txin, tx.vin) - mapLockedInputs.erase(txin.prevout); - - mapLockRequestAccepted.erase(txLockCandidate.txHash); - mapLockRequestRejected.erase(txLockCandidate.txHash); - - BOOST_FOREACH(const CTxLockVote& vote, txLockCandidate.vecTxLockVotes) - if(mapTxLockVotes.count(vote.GetHash())) - mapTxLockVotes.erase(vote.GetHash()); + // remove expired candidates + while(itLockCandidate != mapTxLockCandidates.end()) { + CTxLockCandidate &txLockCandidate = itLockCandidate->second; + uint256 txHash = txLockCandidate.GetHash(); + if(txLockCandidate.IsExpired(pCurrentBlockIndex->nHeight)) { + LogPrintf("CInstantSend::CheckAndRemove -- Removing expired Transaction Lock Candidate: txid=%s\n", txHash.ToString()); + std::map::iterator itOutpointLock = txLockCandidate.mapOutPointLocks.begin(); + while(itOutpointLock != txLockCandidate.mapOutPointLocks.end()) { + mapLockedOutpoints.erase(itOutpointLock->first); + mapVotedOutpoints.erase(itOutpointLock->first); + ++itOutpointLock; } - - mapTxLockCandidates.erase(it++); + mapLockRequestAccepted.erase(txHash); + mapLockRequestRejected.erase(txHash); + mapTxLockCandidates.erase(itLockCandidate++); } else { - ++it; + ++itLockCandidate; } } - // clean expired orphan votes - std::map::iterator it1 = mapTxLockVotesOrphan.begin(); - while(it1 != mapTxLockVotesOrphan.end()) { - if(it1->second.nOrphanExpireTime < GetTime()) { - LogPrint("instantsend", "CleanTxLockCandidates -- Removing expired orphan vote: txid=%s masternode=%s\n", it1->second.txHash.ToString(), it1->second.vinMasternode.prevout.ToStringShort()); - mapTxLockVotesOrphan.erase(it1++); + // remove expired votes + std::map::iterator itVote = mapTxLockVotes.begin(); + while(itVote != mapTxLockVotes.end()) { + if(itVote->second.IsExpired(pCurrentBlockIndex->nHeight)) { + LogPrint("instantsend", "CInstantSend::CheckAndRemove -- Removing expired vote: txid=%s masternode=%s\n", + itVote->second.GetTxHash().ToString(), itVote->second.GetMasternodeOutpoint().ToStringShort()); + mapTxLockVotes.erase(itVote++); } else { - ++it1; + ++itVote; } } - // clean expired masternode orphan vote times - std::map::iterator it2 = mapMasternodeOrphanVotes.begin(); - while(it2 != mapMasternodeOrphanVotes.end()) { - if(it2->second < GetTime()) { - LogPrint("instantsend", "CleanTxLockCandidates -- Removing expired orphan masternode vote time: masternode=%s\n", it2->first.ToStringShort()); - mapMasternodeOrphanVotes.erase(it2++); + // remove expired orphan votes + std::map::iterator itOrphanVote = mapTxLockVotesOrphan.begin(); + while(itOrphanVote != mapTxLockVotesOrphan.end()) { + if(GetTime() - itOrphanVote->second.GetTimeCreated() > ORPHAN_VOTE_SECONDS) { + LogPrint("instantsend", "CInstantSend::CheckAndRemove -- Removing expired orphan vote: txid=%s masternode=%s\n", + itOrphanVote->second.GetTxHash().ToString(), itOrphanVote->second.GetMasternodeOutpoint().ToStringShort()); + mapTxLockVotesOrphan.erase(itOrphanVote++); } else { - ++it2; + ++itOrphanVote; + } + } + + // remove expired masternode orphan votes (DOS protection) + std::map::iterator itMasternodeOrphan = mapMasternodeOrphanVotes.begin(); + while(itMasternodeOrphan != mapMasternodeOrphanVotes.end()) { + if(itMasternodeOrphan->second < GetTime()) { + LogPrint("instantsend", "CInstantSend::CheckAndRemove -- Removing expired orphan masternode vote: masternode=%s\n", + itMasternodeOrphan->first.ToStringShort()); + mapMasternodeOrphanVotes.erase(itMasternodeOrphan++); + } else { + ++itMasternodeOrphan; } } } -bool IsLockedInstandSendTransaction(const uint256 &txHash) +bool CInstantSend::AlreadyHave(const uint256& hash) { LOCK(cs_instantsend); - // there must be a successfully verified lock request... - if (!mapLockRequestAccepted.count(txHash)) return false; - // ...and corresponding lock must have enough signatures - return GetTransactionLockSignatures(txHash) >= INSTANTSEND_SIGNATURES_REQUIRED; + return mapLockRequestAccepted.count(hash) || + mapLockRequestRejected.count(hash) || + mapTxLockVotes.count(hash); } -int GetTransactionLockSignatures(const uint256 &txHash) +void CInstantSend::AcceptLockRequest(const CTxLockRequest& txLockRequest) +{ + LOCK(cs_instantsend); + mapLockRequestAccepted.insert(make_pair(txLockRequest.GetHash(), txLockRequest)); +} + +void CInstantSend::RejectLockRequest(const CTxLockRequest& txLockRequest) +{ + LOCK(cs_instantsend); + mapLockRequestRejected.insert(make_pair(txLockRequest.GetHash(), txLockRequest)); +} + +bool CInstantSend::HasTxLockRequest(const uint256& txHash) +{ + CTxLockRequest txLockRequestTmp; + return GetTxLockRequest(txHash, txLockRequestTmp); +} + +bool CInstantSend::GetTxLockRequest(const uint256& txHash, CTxLockRequest& txLockRequestRet) +{ + LOCK(cs_instantsend); + + std::map::iterator it = mapTxLockCandidates.find(txHash); + if(it == mapTxLockCandidates.end()) return false; + txLockRequestRet = it->second.txLockRequest; + + return true; +} + +bool CInstantSend::GetTxLockVote(const uint256& hash, CTxLockVote& txLockVoteRet) +{ + LOCK(cs_instantsend); + + std::map::iterator it = mapTxLockVotes.find(hash); + if(it == mapTxLockVotes.end()) return false; + txLockVoteRet = it->second; + + return true; +} + +bool CInstantSend::IsInstantSendReadyToLock(const uint256& txHash) +{ + if(!fEnableInstantSend || fLargeWorkForkFound || fLargeWorkInvalidChainFound || + !sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED)) return false; + + LOCK(cs_instantsend); + // There must be a successfully verified lock request + // and all outputs must be locked (i.e. have enough signatures) + std::map::iterator it = mapTxLockCandidates.find(txHash); + return it != mapTxLockCandidates.end() && it->second.IsAllOutPointsReady(); +} + +bool CInstantSend::IsLockedInstantSendTransaction(const uint256& txHash) +{ + if(!fEnableInstantSend || fLargeWorkForkFound || fLargeWorkInvalidChainFound || + !sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED)) return false; + + LOCK(cs_instantsend); + + // there must be a lock candidate + std::map::iterator itLockCandidate = mapTxLockCandidates.find(txHash); + if(itLockCandidate == mapTxLockCandidates.end()) return false; + + // which should have outpoints + if(itLockCandidate->second.mapOutPointLocks.empty()) return false; + + // and all of these outputs must be included in mapLockedOutpoints with correct hash + std::map::iterator itOutpointLock = itLockCandidate->second.mapOutPointLocks.begin(); + while(itOutpointLock != itLockCandidate->second.mapOutPointLocks.end()) { + uint256 hashLocked; + if(!GetLockedOutPointTxHash(itOutpointLock->first, hashLocked) || hashLocked != txHash) return false; + ++itOutpointLock; + } + + return true; +} + +int CInstantSend::GetTransactionLockSignatures(const uint256& txHash) { if(!fEnableInstantSend) return -1; if(fLargeWorkForkFound || fLargeWorkInvalidChainFound) return -2; if(!sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED)) return -3; LOCK(cs_instantsend); - std::map::iterator it = mapTxLockCandidates.find(txHash); - if(it != mapTxLockCandidates.end()) return it->second.CountVotes(); + + std::map::iterator itLockCandidate = mapTxLockCandidates.find(txHash); + if(itLockCandidate != mapTxLockCandidates.end()) { + return itLockCandidate->second.CountVotes(); + } return -1; } -bool IsTransactionLockTimedOut(const uint256 &txHash) +bool CInstantSend::IsTxLockRequestTimedOut(const uint256& txHash) { - if(!fEnableInstantSend) return 0; + if(!fEnableInstantSend) return false; LOCK(cs_instantsend); - std::map::iterator i = mapTxLockCandidates.find(txHash); - if (i != mapTxLockCandidates.end()) return GetTime() > (*i).second.nTimeout; + + std::map::iterator itLockCandidate = mapTxLockCandidates.find(txHash); + if (itLockCandidate != mapTxLockCandidates.end()) { + return !itLockCandidate->second.IsAllOutPointsReady() && + itLockCandidate->second.txLockRequest.IsTimedOut(); + } return false; } -uint256 CTxLockVote::GetHash() const +void CInstantSend::Relay(const uint256& txHash) { - return ArithToUint256(UintToArith256(vinMasternode.prevout.hash) + vinMasternode.prevout.n + UintToArith256(txHash)); + LOCK(cs_instantsend); + + std::map::const_iterator itLockCandidate = mapTxLockCandidates.find(txHash); + if (itLockCandidate != mapTxLockCandidates.end()) { + itLockCandidate->second.Relay(); + } } +void CInstantSend::UpdatedBlockTip(const CBlockIndex *pindex) +{ + pCurrentBlockIndex = pindex; +} + +void CInstantSend::SyncTransaction(const CTransaction& tx, const CBlock* pblock) +{ + // Update lock candidates and votes if corresponding tx confirmed + // or went from confirmed to 0-confirmed or conflicted. + + if (tx.IsCoinBase()) return; + + LOCK2(cs_main, cs_instantsend); + + uint256 txHash = tx.GetHash(); + + // When tx is 0-confirmed or conflicted, pblock is NULL and nHeightNew should be set to -1 + CBlockIndex* pblockindex = pblock ? mapBlockIndex[pblock->GetHash()] : NULL; + int nHeightNew = pblockindex ? pblockindex->nHeight : -1; + + LogPrint("instantsend", "CInstantSend::SyncTransaction -- txid=%s nHeightNew=%d\n", txHash.ToString(), nHeightNew); + + // Check lock candidates + std::map::iterator itLockCandidate = mapTxLockCandidates.find(txHash); + if(itLockCandidate != mapTxLockCandidates.end()) { + LogPrint("instantsend", "CInstantSend::SyncTransaction -- txid=%s nHeightNew=%d lock candidate updated\n", + txHash.ToString(), nHeightNew); + itLockCandidate->second.SetConfirmedHeight(nHeightNew); + } + + // Check lock votes + std::map::iterator itVote = mapTxLockVotes.find(txHash); + if(itVote != mapTxLockVotes.end()) { + LogPrint("instantsend", "CInstantSend::SyncTransaction -- txid=%s nHeightNew=%d vote %s updated\n", + txHash.ToString(), nHeightNew, itVote->second.GetHash().ToString()); + itVote->second.SetConfirmedHeight(nHeightNew); + } + } + +// +// CTxLockRequest +// + +bool CTxLockRequest::IsValid(bool fRequireUnspent) const +{ + if(vout.size() < 1) return false; + + if(vin.size() > MAX_INPUTS) { + LogPrint("instantsend", "CTxLockRequest::IsValid -- Too many inputs: tx=%s", ToString()); + return false; + } + + LOCK(cs_main); + if(!CheckFinalTx(*this)) { + LogPrint("instantsend", "CTxLockRequest::IsValid -- Transaction is not final: tx=%s", ToString()); + return false; + } + + int64_t nValueIn = 0; + int64_t nValueOut = 0; + + BOOST_FOREACH(const CTxOut& txout, vout) { + // InstantSend supports normal scripts and unspendable (i.e. data) scripts. + // TODO: Look into other script types that are normal and can be included + if(!txout.scriptPubKey.IsNormalPaymentScript() && !txout.scriptPubKey.IsUnspendable()) { + LogPrint("instantsend", "CTxLockRequest::IsValid -- Invalid Script %s", ToString()); + return false; + } + nValueOut += txout.nValue; + } + + BOOST_FOREACH(const CTxIn& txin, vin) { + + CCoins coins; + int nPrevoutHeight = 0; + if(!pcoinsTip->GetCoins(txin.prevout.hash, coins) || + (unsigned int)txin.prevout.n>=coins.vout.size() || + coins.vout[txin.prevout.n].IsNull()) { + LogPrint("instantsend", "CTxLockRequest::IsValid -- Failed to find UTXO %s\n", txin.prevout.ToStringShort()); + // Normally above sould be enough, but in case we are reprocessing this because of + // a lot of legit orphan votes we should also check already spent outpoints. + if(fRequireUnspent) return false; + CTransaction txOutpointCreated; + uint256 nHashOutpointConfirmed; + if(!GetTransaction(txin.prevout.hash, txOutpointCreated, Params().GetConsensus(), nHashOutpointConfirmed, true) || nHashOutpointConfirmed == uint256()) { + LogPrint("instantsend", "txLockRequest::IsValid -- Failed to find outpoint %s\n", txin.prevout.ToStringShort()); + return false; + } + LOCK(cs_main); + BlockMap::iterator mi = mapBlockIndex.find(nHashOutpointConfirmed); + if(mi == mapBlockIndex.end()) { + // not on this chain? + LogPrint("instantsend", "txLockRequest::IsValid -- Failed to find block %s for outpoint %s\n", nHashOutpointConfirmed.ToString(), txin.prevout.ToStringShort()); + return false; + } + nPrevoutHeight = mi->second ? mi->second->nHeight : 0; + } + + int nTxAge = chainActive.Height() - (nPrevoutHeight ? nPrevoutHeight : coins.nHeight) + 1; + // 1 less than the "send IX" gui requires, in case of a block propagating the network at the time + int nConfirmationsRequired = INSTANTSEND_CONFIRMATIONS_REQUIRED - 1; + + if(nTxAge < nConfirmationsRequired) { + LogPrint("instantsend", "CTxLockRequest::IsValid -- outpoint %s too new: nTxAge=%d, nConfirmationsRequired=%d, txid=%s\n", + txin.prevout.ToStringShort(), nTxAge, nConfirmationsRequired, GetHash().ToString()); + return false; + } + + nValueIn += coins.vout[txin.prevout.n].nValue; + } + + if(nValueOut > sporkManager.GetSporkValue(SPORK_5_INSTANTSEND_MAX_VALUE)*COIN) { + LogPrint("instantsend", "CTxLockRequest::IsValid -- Transaction value too high: nValueOut=%d, tx=%s", nValueOut, ToString()); + return false; + } + + if(nValueIn - nValueOut < GetMinFee()) { + LogPrint("instantsend", "CTxLockRequest::IsValid -- did not include enough fees in transaction: fees=%d, tx=%s", nValueOut - nValueIn, ToString()); + return false; + } + + return true; +} + +CAmount CTxLockRequest::GetMinFee() const +{ + CAmount nMinFee = MIN_FEE; + return std::max(nMinFee, CAmount(vin.size() * nMinFee)); +} + +int CTxLockRequest::GetMaxSignatures() const +{ + return vin.size() * COutPointLock::SIGNATURES_TOTAL; +} + +bool CTxLockRequest::IsTimedOut() const +{ + return GetTime() - nTimeCreated > TIMEOUT_SECONDS; +} + +// +// CTxLockVote +// + +bool CTxLockVote::IsValid(CNode* pnode) const +{ + if(!mnodeman.Has(CTxIn(outpointMasternode))) { + LogPrint("instantsend", "CTxLockVote::IsValid -- Unknown masternode %s\n", outpointMasternode.ToStringShort()); + mnodeman.AskForMN(pnode, CTxIn(outpointMasternode)); + return false; + } + + int nPrevoutHeight = GetUTXOHeight(outpoint); + if(nPrevoutHeight == -1) { + LogPrint("instantsend", "CTxLockVote::IsValid -- Failed to find UTXO %s\n", outpoint.ToStringShort()); + // Validating utxo set is not enough, votes can arrive after outpoint was already spent, + // if lock request was mined. We should process them too to count them later if they are legit. + CTransaction txOutpointCreated; + uint256 nHashOutpointConfirmed; + if(!GetTransaction(outpoint.hash, txOutpointCreated, Params().GetConsensus(), nHashOutpointConfirmed, true) || nHashOutpointConfirmed == uint256()) { + LogPrint("instantsend", "CTxLockVote::IsValid -- Failed to find outpoint %s\n", outpoint.ToStringShort()); + return false; + } + LOCK(cs_main); + BlockMap::iterator mi = mapBlockIndex.find(nHashOutpointConfirmed); + if(mi == mapBlockIndex.end() || !mi->second) { + // not on this chain? + LogPrint("instantsend", "CTxLockVote::IsValid -- Failed to find block %s for outpoint %s\n", nHashOutpointConfirmed.ToString(), outpoint.ToStringShort()); + return false; + } + nPrevoutHeight = mi->second->nHeight; + } + + int nLockInputHeight = nPrevoutHeight + 4; + + int n = mnodeman.GetMasternodeRank(CTxIn(outpointMasternode), nLockInputHeight, MIN_INSTANTSEND_PROTO_VERSION); + + if(n == -1) { + //can be caused by past versions trying to vote with an invalid protocol + LogPrint("instantsend", "CTxLockVote::IsValid -- Outdated masternode %s\n", outpointMasternode.ToStringShort()); + return false; + } + LogPrint("instantsend", "CTxLockVote::IsValid -- Masternode %s, rank=%d\n", outpointMasternode.ToStringShort(), n); + + int nSignaturesTotal = COutPointLock::SIGNATURES_TOTAL; + if(n > nSignaturesTotal) { + LogPrint("instantsend", "CTxLockVote::IsValid -- Masternode %s is not in the top %d (%d), vote hash=%s\n", + outpointMasternode.ToStringShort(), nSignaturesTotal, n, GetHash().ToString()); + return false; + } + + if(!CheckSignature()) { + LogPrintf("CTxLockVote::IsValid -- Signature invalid\n"); + return false; + } + + return true; +} + +uint256 CTxLockVote::GetHash() const +{ + CHashWriter ss(SER_GETHASH, PROTOCOL_VERSION); + ss << txHash; + ss << outpoint; + ss << outpointMasternode; + return ss.GetHash(); +} bool CTxLockVote::CheckSignature() const { std::string strError; - std::string strMessage = txHash.ToString().c_str() + boost::lexical_cast(nBlockHeight); + std::string strMessage = txHash.ToString() + outpoint.ToStringShort(); - masternode_info_t infoMn = mnodeman.GetMasternodeInfo(vinMasternode); + masternode_info_t infoMn = mnodeman.GetMasternodeInfo(CTxIn(outpointMasternode)); if(!infoMn.fInfoValid) { - LogPrintf("CTxLockVote::CheckSignature -- Unknown Masternode: txin=%s\n", vinMasternode.ToString()); + LogPrintf("CTxLockVote::CheckSignature -- Unknown Masternode: masternode=%s\n", outpointMasternode.ToString()); return false; } - if(!darkSendSigner.VerifyMessage(infoMn.pubKeyMasternode, vchMasterNodeSignature, strMessage, strError)) { + if(!darkSendSigner.VerifyMessage(infoMn.pubKeyMasternode, vchMasternodeSignature, strMessage, strError)) { LogPrintf("CTxLockVote::CheckSignature -- VerifyMessage() failed, error: %s\n", strError); return false; } @@ -590,15 +992,14 @@ bool CTxLockVote::CheckSignature() const bool CTxLockVote::Sign() { std::string strError; + std::string strMessage = txHash.ToString() + outpoint.ToStringShort(); - std::string strMessage = txHash.ToString().c_str() + boost::lexical_cast(nBlockHeight); - - if(!darkSendSigner.SignMessage(strMessage, vchMasterNodeSignature, activeMasternode.keyMasternode)) { + if(!darkSendSigner.SignMessage(strMessage, vchMasternodeSignature, activeMasternode.keyMasternode)) { LogPrintf("CTxLockVote::Sign -- SignMessage() failed\n"); return false; } - if(!darkSendSigner.VerifyMessage(activeMasternode.pubKeyMasternode, vchMasterNodeSignature, strMessage, strError)) { + if(!darkSendSigner.VerifyMessage(activeMasternode.pubKeyMasternode, vchMasternodeSignature, strMessage, strError)) { LogPrintf("CTxLockVote::Sign -- VerifyMessage() failed, error: %s\n", strError); return false; } @@ -606,51 +1007,103 @@ bool CTxLockVote::Sign() return true; } - -bool CTxLockCandidate::IsAllVotesValid() +void CTxLockVote::Relay() const { + CInv inv(MSG_TXLOCK_VOTE, GetHash()); + RelayInv(inv); +} - BOOST_FOREACH(const CTxLockVote& vote, vecTxLockVotes) - { - int n = mnodeman.GetMasternodeRank(vote.vinMasternode, vote.nBlockHeight, MIN_INSTANTSEND_PROTO_VERSION); +bool CTxLockVote::IsExpired(int nHeight) const +{ + // Locks and votes expire nInstantSendKeepLock blocks after the block corresponding tx was included into. + return (nConfirmedHeight != -1) && (nHeight - nConfirmedHeight > Params().GetConsensus().nInstantSendKeepLock); +} - if(n == -1) { - LogPrintf("CTxLockCandidate::IsAllVotesValid -- Unknown Masternode, txin=%s\n", vote.vinMasternode.ToString()); - return false; - } - - if(n > INSTANTSEND_SIGNATURES_TOTAL) { - LogPrintf("CTxLockCandidate::IsAllVotesValid -- Masternode not in the top %s\n", INSTANTSEND_SIGNATURES_TOTAL); - return false; - } - - if(!vote.CheckSignature()) { - LogPrintf("CTxLockCandidate::IsAllVotesValid -- Signature not valid\n"); - return false; - } - } +// +// COutPointLock +// +bool COutPointLock::AddVote(const CTxLockVote& vote) +{ + if(mapMasternodeVotes.count(vote.GetMasternodeOutpoint())) + return false; + mapMasternodeVotes.insert(std::make_pair(vote.GetMasternodeOutpoint(), vote)); return true; } -void CTxLockCandidate::AddVote(const CTxLockVote& vote) +bool COutPointLock::HasMasternodeVoted(const COutPoint& outpointMasternodeIn) { - vecTxLockVotes.push_back(vote); + return mapMasternodeVotes.count(outpointMasternodeIn); } -int CTxLockCandidate::CountVotes() +void COutPointLock::Relay() const { - /* - Only count signatures where the BlockHeight matches the transaction's blockheight. - The votes have no proof it's the correct blockheight - */ - - if(nBlockHeight == 0) return -1; - - int nCount = 0; - BOOST_FOREACH(const CTxLockVote& vote, vecTxLockVotes) - if(vote.nBlockHeight == nBlockHeight) - nCount++; - - return nCount; + std::map::const_iterator itVote = mapMasternodeVotes.begin(); + while(itVote != mapMasternodeVotes.end()) { + itVote->second.Relay(); + ++itVote; + } +} + +// +// CTxLockCandidate +// + +void CTxLockCandidate::AddOutPointLock(const COutPoint& outpoint) +{ + mapOutPointLocks.insert(make_pair(outpoint, COutPointLock(outpoint))); +} + + +bool CTxLockCandidate::AddVote(const CTxLockVote& vote) +{ + std::map::iterator it = mapOutPointLocks.find(vote.GetOutpoint()); + if(it == mapOutPointLocks.end()) return false; + return it->second.AddVote(vote); +} + +bool CTxLockCandidate::IsAllOutPointsReady() const +{ + if(mapOutPointLocks.empty()) return false; + + std::map::const_iterator it = mapOutPointLocks.begin(); + while(it != mapOutPointLocks.end()) { + if(!it->second.IsReady()) return false; + ++it; + } + return true; +} + +bool CTxLockCandidate::HasMasternodeVoted(const COutPoint& outpointIn, const COutPoint& outpointMasternodeIn) +{ + std::map::iterator it = mapOutPointLocks.find(outpointIn); + return it !=mapOutPointLocks.end() && it->second.HasMasternodeVoted(outpointMasternodeIn); +} + +int CTxLockCandidate::CountVotes() const +{ + // Note: do NOT use vote count to figure out if tx is locked, use IsAllOutPointsReady() instead + int nCountVotes = 0; + std::map::const_iterator it = mapOutPointLocks.begin(); + while(it != mapOutPointLocks.end()) { + nCountVotes += it->second.CountVotes(); + ++it; + } + return nCountVotes; +} + +bool CTxLockCandidate::IsExpired(int nHeight) const +{ + // Locks and votes expire nInstantSendKeepLock blocks after the block corresponding tx was included into. + return (nConfirmedHeight != -1) && (nHeight - nConfirmedHeight > Params().GetConsensus().nInstantSendKeepLock); +} + +void CTxLockCandidate::Relay() const +{ + RelayTransaction(txLockRequest); + std::map::const_iterator itOutpointLock = mapOutPointLocks.begin(); + while(itOutpointLock != mapOutPointLocks.end()) { + itOutpointLock->second.Relay(); + ++itOutpointLock; + } } diff --git a/src/instantx.h b/src/instantx.h index 8cc959280..306b4a50a 100644 --- a/src/instantx.h +++ b/src/instantx.h @@ -7,9 +7,13 @@ #include "net.h" #include "primitives/transaction.h" -class CTransaction; class CTxLockVote; +class COutPointLock; +class CTxLockRequest; class CTxLockCandidate; +class CInstantSend; + +extern CInstantSend instantsend; /* At 15 signatures, 1/2 of the masternode network can be owned by @@ -20,106 +24,225 @@ class CTxLockCandidate; ### getting 5 of 10 signatures w/ 1000 nodes of 2900 (1000/2900.0)**5 = 0.004875397277841433 */ -static const int INSTANTSEND_SIGNATURES_REQUIRED = 6; -static const int INSTANTSEND_SIGNATURES_TOTAL = 10; +static const int INSTANTSEND_CONFIRMATIONS_REQUIRED = 6; static const int DEFAULT_INSTANTSEND_DEPTH = 5; -static const int MIN_INSTANTSEND_PROTO_VERSION = 70204; -static const CAmount INSTANTSEND_MIN_FEE = 0.001 * COIN; +static const int MIN_INSTANTSEND_PROTO_VERSION = 70205; extern bool fEnableInstantSend; extern int nInstantSendDepth; extern int nCompleteTXLocks; -extern std::map mapLockRequestAccepted; -extern std::map mapLockRequestRejected; -extern std::map mapTxLockVotes; -extern std::map mapLockedInputs; +class CInstantSend +{ +private: + static const int ORPHAN_VOTE_SECONDS = 60; + // Keep track of current block index + const CBlockIndex *pCurrentBlockIndex; -void ProcessMessageInstantSend(CNode* pfrom, std::string& strCommand, CDataStream& vRecv); + // maps for AlreadyHave + std::map mapLockRequestAccepted; // tx hash - tx + std::map mapLockRequestRejected; // tx hash - tx + std::map mapTxLockVotes; // vote hash - vote + std::map mapTxLockVotesOrphan; // vote hash - vote -bool IsInstantSendTxValid(const CTransaction& txCandidate); + std::map mapTxLockCandidates; // tx hash - lock candidate -bool ProcessTxLockRequest(CNode* pfrom, const CTransaction &tx); + std::map > mapVotedOutpoints; // utxo - tx hash set + std::map mapLockedOutpoints; // utxo - tx hash -int64_t CreateTxLockCandidate(const CTransaction &tx); + //track masternodes who voted with no txreq (for DOS protection) + std::map mapMasternodeOrphanVotes; // mn outpoint - time -//check if we need to vote on this transaction -void CreateTxLockVote(const CTransaction& tx, int nBlockHeight); + bool CreateTxLockCandidate(const CTxLockRequest& txLockRequest); + void Vote(CTxLockCandidate& txLockCandidate); -//process consensus vote message -bool ProcessTxLockVote(CNode *pnode, CTxLockVote& vote); + //process consensus vote message + bool ProcessTxLockVote(CNode* pfrom, CTxLockVote& vote); + void ProcessOrphanTxLockVotes(); + bool IsEnoughOrphanVotesForTx(const CTxLockRequest& txLockRequest); + bool IsEnoughOrphanVotesForTxAndOutPoint(const uint256& txHash, const COutPoint& outpoint); + int64_t GetAverageMasternodeOrphanVoteTime(); -void ProcessOrphanTxLockVotes(); + void TryToFinalizeLockCandidate(const CTxLockCandidate& txLockCandidate); + void LockTransactionInputs(const CTxLockCandidate& txLockCandidate); + //update UI and notify external script if any + void UpdateLockedTransaction(const CTxLockCandidate& txLockCandidate); + bool ResolveConflicts(const CTxLockCandidate& txLockCandidate, int nMaxBlocks); -//update UI and notify external script if any -void UpdateLockedTransaction(const CTransaction& tx, bool fForceNotification = false); + bool IsInstantSendReadyToLock(const uint256 &txHash); -void LockTransactionInputs(const CTransaction& tx); +public: + CCriticalSection cs_instantsend; -// if two conflicting locks are approved by the network, they will cancel out -bool FindConflictingLocks(const CTransaction& tx); + void ProcessMessage(CNode* pfrom, std::string& strCommand, CDataStream& vRecv); -//try to resolve conflicting locks -void ResolveConflicts(const CTransaction& tx); + bool ProcessTxLockRequest(const CTxLockRequest& txLockRequest); -// keep transaction locks in memory for an hour -void CleanTxLockCandidates(); + bool AlreadyHave(const uint256& hash); -// verify if transaction is currently locked -bool IsLockedInstandSendTransaction(const uint256 &txHash); + void AcceptLockRequest(const CTxLockRequest& txLockRequest); + void RejectLockRequest(const CTxLockRequest& txLockRequest); + bool HasTxLockRequest(const uint256& txHash); + bool GetTxLockRequest(const uint256& txHash, CTxLockRequest& txLockRequestRet); -// get the actual uber og accepted lock signatures -int GetTransactionLockSignatures(const uint256 &txHash); + bool GetTxLockVote(const uint256& hash, CTxLockVote& txLockVoteRet); -// verify if transaction lock timed out -bool IsTransactionLockTimedOut(const uint256 &txHash); + bool GetLockedOutPointTxHash(const COutPoint& outpoint, uint256& hashRet); -int64_t GetAverageMasternodeOrphanVoteTime(); + // verify if transaction is currently locked + bool IsLockedInstantSendTransaction(const uint256& txHash); + // get the actual uber og accepted lock signatures + int GetTransactionLockSignatures(const uint256& txHash); + + // remove expired entries from maps + void CheckAndRemove(); + // verify if transaction lock timed out + bool IsTxLockRequestTimedOut(const uint256& txHash); + + void Relay(const uint256& txHash); + + void UpdatedBlockTip(const CBlockIndex *pindex); + void SyncTransaction(const CTransaction& tx, const CBlock* pblock); +}; + +class CTxLockRequest : public CTransaction +{ +private: + static const int TIMEOUT_SECONDS = 5 * 60; + static const CAmount MIN_FEE = 0.001 * COIN; + + int64_t nTimeCreated; + +public: + static const int MAX_INPUTS = 10; + + CTxLockRequest() : + CTransaction(), + nTimeCreated(GetTime()) + {} + CTxLockRequest(const CTransaction& tx) : + CTransaction(tx), + nTimeCreated(GetTime()) + {} + + bool IsValid(bool fRequireUnspent = true) const; + CAmount GetMinFee() const; + int GetMaxSignatures() const; + bool IsTimedOut() const; +}; class CTxLockVote { -public: - CTxIn vinMasternode; +private: uint256 txHash; - int nBlockHeight; - std::vector vchMasterNodeSignature; - + COutPoint outpoint; + COutPoint outpointMasternode; + std::vector vchMasternodeSignature; // local memory only - int64_t nOrphanExpireTime; + int nConfirmedHeight; // when corresponding tx is 0-confirmed or conflicted, nConfirmedHeight is -1 + int64_t nTimeCreated; + +public: + CTxLockVote() : + txHash(), + outpoint(), + outpointMasternode(), + vchMasternodeSignature(), + nConfirmedHeight(-1), + nTimeCreated(GetTime()) + {} + + CTxLockVote(const uint256& txHashIn, const COutPoint& outpointIn, const COutPoint& outpointMasternodeIn) : + txHash(txHashIn), + outpoint(outpointIn), + outpointMasternode(outpointMasternodeIn), + vchMasternodeSignature(), + nConfirmedHeight(-1), + nTimeCreated(GetTime()) + {} ADD_SERIALIZE_METHODS; template inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) { READWRITE(txHash); - READWRITE(vinMasternode); - READWRITE(vchMasterNodeSignature); - READWRITE(nBlockHeight); + READWRITE(outpoint); + READWRITE(outpointMasternode); + READWRITE(vchMasternodeSignature); } uint256 GetHash() const; + uint256 GetTxHash() const { return txHash; } + COutPoint GetOutpoint() const { return outpoint; } + COutPoint GetMasternodeOutpoint() const { return outpointMasternode; } + int64_t GetTimeCreated() const { return nTimeCreated; } + + bool IsValid(CNode* pnode) const; + void SetConfirmedHeight(int nConfirmedHeightIn) { nConfirmedHeight = nConfirmedHeightIn; } + bool IsExpired(int nHeight) const; + bool Sign(); bool CheckSignature() const; + + void Relay() const; +}; + +class COutPointLock +{ +private: + COutPoint outpoint; // utxo + std::map mapMasternodeVotes; // masternode outpoint - vote + +public: + static const int SIGNATURES_REQUIRED = 6; + static const int SIGNATURES_TOTAL = 10; + + COutPointLock(const COutPoint& outpointIn) : + outpoint(outpointIn), + mapMasternodeVotes() + {} + + COutPoint GetOutpoint() const { return outpoint; } + + bool AddVote(const CTxLockVote& vote); + bool HasMasternodeVoted(const COutPoint& outpointMasternodeIn); + int CountVotes() const { return mapMasternodeVotes.size(); } + bool IsReady() const { return CountVotes() >= SIGNATURES_REQUIRED; } + + void Relay() const; }; class CTxLockCandidate { +private: + int nConfirmedHeight; // when corresponding tx is 0-confirmed or conflicted, nConfirmedHeight is -1 + public: - int nBlockHeight; - uint256 txHash; - std::vector vecTxLockVotes; - int nExpirationBlock; - int nTimeout; + CTxLockCandidate(const CTxLockRequest& txLockRequestIn) : + nConfirmedHeight(-1), + txLockRequest(txLockRequestIn), + mapOutPointLocks() + {} - uint256 GetHash() const { return txHash; } + CTxLockRequest txLockRequest; + std::map mapOutPointLocks; - bool IsAllVotesValid(); - void AddVote(const CTxLockVote& vote); - int CountVotes(); + uint256 GetHash() const { return txLockRequest.GetHash(); } + + void AddOutPointLock(const COutPoint& outpoint); + bool AddVote(const CTxLockVote& vote); + bool IsAllOutPointsReady() const; + + bool HasMasternodeVoted(const COutPoint& outpointIn, const COutPoint& outpointMasternodeIn); + int CountVotes() const; + + void SetConfirmedHeight(int nConfirmedHeightIn) { nConfirmedHeight = nConfirmedHeightIn; } + bool IsExpired(int nHeight) const; + + void Relay() const; }; - #endif diff --git a/src/main.cpp b/src/main.cpp index fbcc40fa9..27741bc8d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -941,6 +941,18 @@ unsigned int GetP2SHSigOpCount(const CTransaction& tx, const CCoinsViewCache& in return nSigOps; } +int GetUTXOHeight(const COutPoint& outpoint) +{ + LOCK(cs_main); + CCoins coins; + if(!pcoinsTip->GetCoins(outpoint.hash, coins) || + (unsigned int)outpoint.n>=coins.vout.size() || + coins.vout[outpoint.n].IsNull()) { + return -1; + } + return coins.nHeight; +} + int GetInputAge(const CTxIn &txin) { CCoinsView viewDummy; @@ -966,7 +978,7 @@ int GetInputAgeIX(const uint256 &nTXHash, const CTxIn &txin) int nResult = GetInputAge(txin); if(nResult < 0) return -1; - if (nResult < 6 && IsLockedInstandSendTransaction(nTXHash)) + if (nResult < 6 && instantsend.IsLockedInstantSendTransaction(nTXHash)) return nInstantSendDepth + nResult; return nResult; @@ -974,7 +986,7 @@ int GetInputAgeIX(const uint256 &nTXHash, const CTxIn &txin) int GetIXConfirmations(const uint256 &nTXHash) { - if (IsLockedInstandSendTransaction(nTXHash)) + if (instantsend.IsLockedInstantSendTransaction(nTXHash)) return nInstantSendDepth; return 0; @@ -1088,14 +1100,19 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, const C if (pool.exists(hash)) return state.Invalid(false, REJECT_ALREADY_KNOWN, "txn-already-in-mempool"); - // ----------- InstantSend transaction scanning ----------- + // If this is a Transaction Lock Request check to see if it's valid + if(instantsend.HasTxLockRequest(hash) && !CTxLockRequest(tx).IsValid()) + return state.DoS(10, error("AcceptToMemoryPool : CTxLockRequest %s is invalid", hash.ToString()), + REJECT_INVALID, "bad-txlockrequest"); - BOOST_FOREACH(const CTxIn& txin, tx.vin) { - if(mapLockedInputs.count(txin.prevout) && mapLockedInputs[txin.prevout] != tx.GetHash()) { - return state.DoS(0, - error("AcceptToMemoryPool : conflicts with existing transaction lock: %s", reason), - REJECT_INVALID, "tx-lock-conflict"); - } + // Check for conflicts with a completed Transaction Lock + BOOST_FOREACH(const CTxIn &txin, tx.vin) + { + uint256 hashLocked; + if(instantsend.GetLockedOutPointTxHash(txin.prevout, hashLocked) && hash != hashLocked) + return state.DoS(10, error("AcceptToMemoryPool : Transaction %s conflicts with completed Transaction Lock %s", + hash.ToString(), hashLocked.ToString()), + REJECT_INVALID, "tx-txlock-conflict"); } // Check for conflicts with in-memory transactions @@ -1109,6 +1126,18 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, const C const CTransaction *ptxConflicting = pool.mapNextTx[txin.prevout].ptx; if (!setConflicts.count(ptxConflicting->GetHash())) { + // InstantSend txes are not replacable + if(instantsend.HasTxLockRequest(ptxConflicting->GetHash())) { + // this tx conflicts with a Transaction Lock Request candidate + return state.DoS(0, error("AcceptToMemoryPool : Transaction %s conflicts with Transaction Lock Request %s", + hash.ToString(), ptxConflicting->GetHash().ToString()), + REJECT_INVALID, "tx-txlockreq-mempool-conflict"); + } else if (instantsend.HasTxLockRequest(hash)) { + // this tx is a tx lock request and it conflicts with a normal tx + return state.DoS(0, error("AcceptToMemoryPool : Transaction Lock Request %s conflicts with transaction %s", + hash.ToString(), ptxConflicting->GetHash().ToString()), + REJECT_INVALID, "txlockreq-tx-mempool-conflict"); + } // Allow opt-out of transaction replacement by setting // nSequence >= maxint-1 on all inputs. // @@ -3701,24 +3730,33 @@ bool CheckBlock(const CBlock& block, CValidationState& state, bool fCheckPOW, bo REJECT_INVALID, "bad-cb-multiple"); - // DASH : CHECK TRANSACTIONS FOR INSTANT SEND + // DASH : CHECK TRANSACTIONS FOR INSTANTSEND if(sporkManager.IsSporkActive(SPORK_3_INSTANTSEND_BLOCK_FILTERING)) { + // We should never accept block which conflicts with completed transaction lock, + // that's why this is in CheckBlock unlike coinbase payee/amount. + // Require other nodes to comply, send them some data in case they are missing it. BOOST_FOREACH(const CTransaction& tx, block.vtx) { // skip coinbase, it has no inputs if (tx.IsCoinBase()) continue; - // LOOK FOR TRANSACTION LOCK IN OUR MAP OF INPUTS + // LOOK FOR TRANSACTION LOCK IN OUR MAP OF OUTPOINTS BOOST_FOREACH(const CTxIn& txin, tx.vin) { - if(mapLockedInputs.count(txin.prevout) && mapLockedInputs[txin.prevout] != tx.GetHash()) { + uint256 hashLocked; + if(instantsend.GetLockedOutPointTxHash(txin.prevout, hashLocked) && hashLocked != tx.GetHash()) { + // Every node which relayed this block to us must invalidate it + // but they probably need more data. + // Relay corresponding transaction lock request and all its votes + // to let other nodes complete the lock. + instantsend.Relay(hashLocked); mapRejectedBlocks.insert(make_pair(block.GetHash(), GetTime())); - LogPrintf("CheckBlock(DASH): found conflicting transaction with transaction lock %s %s\n", mapLockedInputs[txin.prevout].ToString(), tx.GetHash().ToString()); - return state.DoS(0, error("CheckBlock(DASH): found conflicting transaction with transaction lock"), - REJECT_INVALID, "conflicting-tx-ix"); + return state.DoS(0, error("CheckBlock(DASH): transaction %s conflicts with transaction lock %s", + tx.GetHash().ToString(), hashLocked.ToString()), + REJECT_INVALID, "conflict-tx-lock"); } } } } else { - LogPrintf("CheckBlock(DASH): skipping transaction locking checks\n"); + LogPrintf("CheckBlock(DASH): spork is off, skipping transaction locking checks\n"); } // END DASH @@ -4901,10 +4939,10 @@ bool static AlreadyHave(const CInv& inv) EXCLUSIVE_LOCKS_REQUIRED(cs_main) We want to only update the time on new hits, so that we can time out appropriately if needed. */ case MSG_TXLOCK_REQUEST: - return mapLockRequestAccepted.count(inv.hash) || mapLockRequestRejected.count(inv.hash); + return instantsend.AlreadyHave(inv.hash); case MSG_TXLOCK_VOTE: - return mapTxLockVotes.count(inv.hash); + return instantsend.AlreadyHave(inv.hash); case MSG_SPORK: return mapSporks.count(inv.hash); @@ -5063,20 +5101,22 @@ void static ProcessGetData(CNode* pfrom, const Consensus::Params& consensusParam } if (!pushed && inv.type == MSG_TXLOCK_REQUEST) { - if(mapLockRequestAccepted.count(inv.hash)) { + CTxLockRequest txLockRequest; + if(instantsend.GetTxLockRequest(inv.hash, txLockRequest)) { CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss.reserve(1000); - ss << mapLockRequestAccepted[inv.hash]; + ss << txLockRequest; pfrom->PushMessage(NetMsgType::TXLOCKREQUEST, ss); pushed = true; } } if (!pushed && inv.type == MSG_TXLOCK_VOTE) { - if(mapTxLockVotes.count(inv.hash)) { + CTxLockVote vote; + if(instantsend.GetTxLockVote(inv.hash, vote)) { CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss.reserve(1000); - ss << mapTxLockVotes[inv.hash]; + ss << vote; pfrom->PushMessage(NetMsgType::TXLOCKVOTE, ss); pushed = true; } @@ -5687,13 +5727,16 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, vector vWorkQueue; vector vEraseQueue; CTransaction tx; + CTxLockRequest txLockRequest; CDarksendBroadcastTx dstx; int nInvType = MSG_TX; + // Read data and assign inv type if(strCommand == NetMsgType::TX) { vRecv >> tx; } else if(strCommand == NetMsgType::TXLOCKREQUEST) { - vRecv >> tx; + vRecv >> txLockRequest; + tx = txLockRequest; nInvType = MSG_TXLOCK_REQUEST; } else if (strCommand == NetMsgType::DSTX) { vRecv >> dstx; @@ -5705,7 +5748,13 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, pfrom->AddInventoryKnown(inv); pfrom->setAskFor.erase(inv.hash); - if (strCommand == NetMsgType::DSTX) { + // Process custom logic, no matter if tx will be accepted to mempool later or not + if (strCommand == NetMsgType::TXLOCKREQUEST) { + if(!instantsend.ProcessTxLockRequest(txLockRequest)) { + LogPrint("instantsend", "TXLOCKREQUEST -- failed %s\n", txLockRequest.GetHash().ToString()); + return false; + } + } else if (strCommand == NetMsgType::DSTX) { uint256 hashTx = tx.GetHash(); if(mapDarksendBroadcastTxes.count(hashTx)) { @@ -5745,11 +5794,15 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, if (!AlreadyHave(inv) && AcceptToMemoryPool(mempool, state, tx, true, &fMissingInputs)) { - // Process custom txes + // Process custom txes, this changes AlreadyHave to "true" if (strCommand == NetMsgType::DSTX) { + LogPrintf("DSTX -- Masternode transaction accepted, txid=%s, peer=%d\n", + tx.GetHash().ToString(), pfrom->id); mapDarksendBroadcastTxes.insert(make_pair(tx.GetHash(), dstx)); } else if (strCommand == NetMsgType::TXLOCKREQUEST) { - if(!ProcessTxLockRequest(pfrom, tx)) return false; + LogPrintf("TXLOCKREQUEST -- Transaction Lock Request accepted, txid=%s, peer=%d\n", + tx.GetHash().ToString(), pfrom->id); + instantsend.AcceptLockRequest(txLockRequest); } mempool.check(pcoinsTip); @@ -5828,18 +5881,17 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, assert(recentRejects); recentRejects->insert(tx.GetHash()); - if (strCommand == NetMsgType::TXLOCKREQUEST && !AlreadyHave(inv)) { // i.e. AcceptToMemoryPool failed - mapLockRequestRejected.insert(std::make_pair(tx.GetHash(), tx)); + if (strCommand == NetMsgType::TXLOCKREQUEST && !AlreadyHave(inv)) { + // i.e. AcceptToMemoryPool failed, probably because it's conflicting + // with existing normal tx or tx lock for another tx. For the same tx lock + // AlreadyHave would have return "true" already. - // can we get the conflicting transaction as proof? - - LogPrintf("TXLOCKREQUEST -- Transaction Lock Request: %s %s : rejected %s\n", - pfrom->addr.ToString(), pfrom->cleanSubVer, - tx.GetHash().ToString() - ); - - LockTransactionInputs(tx); - ResolveConflicts(tx); + // It's the first time we failed for this tx lock request, + // this should switch AlreadyHave to "true". + instantsend.RejectLockRequest(txLockRequest); + // this lets other nodes to create lock request candidate i.e. + // this allows multiple conflicting lock requests to compete for votes + RelayTransaction(tx); } if (pfrom->fWhitelisted && GetBoolArg("-whitelistforcerelay", DEFAULT_WHITELISTFORCERELAY)) { @@ -6259,7 +6311,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, darkSendPool.ProcessMessage(pfrom, strCommand, vRecv); mnodeman.ProcessMessage(pfrom, strCommand, vRecv); mnpayments.ProcessMessage(pfrom, strCommand, vRecv); - ProcessMessageInstantSend(pfrom, strCommand, vRecv); + instantsend.ProcessMessage(pfrom, strCommand, vRecv); sporkManager.ProcessSpork(pfrom, strCommand, vRecv); masternodeSync.ProcessMessage(pfrom, strCommand, vRecv); governance.ProcessMessage(pfrom, strCommand, vRecv); diff --git a/src/main.h b/src/main.h index 9d7dbddb4..d0a7bf10d 100644 --- a/src/main.h +++ b/src/main.h @@ -291,6 +291,7 @@ void PruneAndFlush(); bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransaction &tx, bool fLimitFree, bool* pfMissingInputs, bool fOverrideMempoolLimit=false, bool fRejectAbsurdFee=false, bool fDryRun=false); +int GetUTXOHeight(const COutPoint& outpoint); int GetInputAge(const CTxIn &txin); int GetInputAgeIX(const uint256 &nTXHash, const CTxIn &txin); int GetIXConfirmations(const uint256 &nTXHash); diff --git a/src/net.cpp b/src/net.cpp index 6e78ff37e..4f2c1e4e1 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -2111,10 +2111,11 @@ void RelayTransaction(const CTransaction& tx) CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss.reserve(10000); uint256 hash = tx.GetHash(); + CTxLockRequest txLockRequest; if(mapDarksendBroadcastTxes.count(hash)) { // MSG_DSTX ss << mapDarksendBroadcastTxes[hash]; - } else if(mapLockRequestAccepted.count(hash)) { // MSG_TXLOCK_REQUEST - ss << mapLockRequestAccepted[hash]; + } else if(instantsend.GetTxLockRequest(hash, txLockRequest)) { // MSG_TXLOCK_REQUEST + ss << txLockRequest; } else { // MSG_TX ss << tx; } @@ -2125,7 +2126,7 @@ void RelayTransaction(const CTransaction& tx, const CDataStream& ss) { uint256 hash = tx.GetHash(); int nInv = mapDarksendBroadcastTxes.count(hash) ? MSG_DSTX : - (mapLockRequestAccepted.count(hash) ? MSG_TXLOCK_REQUEST : MSG_TX); + (instantsend.HasTxLockRequest(hash) ? MSG_TXLOCK_REQUEST : MSG_TX); CInv inv(nInv, hash); { LOCK(cs_mapRelay); diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index cd15edb8b..c58c5b48a 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -585,6 +585,10 @@ void CoinControlDialog::updateLabels(WalletModel *model, QDialog* dialog) nBytesInputs += 148; // in all error cases, simply assume 148 here } else nBytesInputs += 148; + + // Add inputs to calculate InstantSend Fee later + if(coinControl->fUseInstantSend) + txDummy.vin.push_back(CTxIn()); } // calculation @@ -609,7 +613,7 @@ void CoinControlDialog::updateLabels(WalletModel *model, QDialog* dialog) nPayFee = coinControl->nMinimumTotalFee; // InstantSend Fee - if (coinControl->fUseInstantSend) nPayFee = std::max(nPayFee, INSTANTSEND_MIN_FEE); + if (coinControl->fUseInstantSend) nPayFee = std::max(nPayFee, CTxLockRequest(txDummy).GetMinFee()); // Allow free? (require at least hard-coded threshold and default to that if no estimate) double dPriorityNeeded = std::max(mempoolEstimatePriority, AllowFreeThreshold()); diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp index d726dac22..27f5d47dd 100644 --- a/src/qt/transactiondesc.cpp +++ b/src/qt/transactiondesc.cpp @@ -41,7 +41,6 @@ QString TransactionDesc::FormatTxStatus(const CWalletTx& wtx) QString strTxStatus; bool fOffline = (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60) && (wtx.GetRequestCount() == 0); - int nSignatures = GetTransactionLockSignatures(wtx.GetHash()); if (fOffline) { strTxStatus = tr("%1/offline").arg(nDepth); @@ -51,14 +50,16 @@ QString TransactionDesc::FormatTxStatus(const CWalletTx& wtx) strTxStatus = tr("%1 confirmations").arg(nDepth); } - if(nSignatures < 0) return strTxStatus; // regular tx + if(!instantsend.HasTxLockRequest(wtx.GetHash())) return strTxStatus; // regular tx + int nSignatures = instantsend.GetTransactionLockSignatures(wtx.GetHash()); + int nSignaturesMax = CTxLockRequest(wtx).GetMaxSignatures(); // InstantSend strTxStatus += " ("; - if(nSignatures >= INSTANTSEND_SIGNATURES_REQUIRED) { + if(instantsend.IsLockedInstantSendTransaction(wtx.GetHash())) { strTxStatus += tr("verified via InstantSend"); - } else if(!IsTransactionLockTimedOut(wtx.GetHash())) { - strTxStatus += tr("InstantSend verification in progress - %1 of %2 signatures").arg(nSignatures).arg(INSTANTSEND_SIGNATURES_TOTAL); + } else if(!instantsend.IsTxLockRequestTimedOut(wtx.GetHash())) { + strTxStatus += tr("InstantSend verification in progress - %1 of %2 signatures").arg(nSignatures).arg(nSignaturesMax); } else { strTxStatus += tr("InstantSend verification failed"); } diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 7ac91275c..33ccca12a 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -307,10 +307,17 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact if (fSubtractFeeFromAmount && fCreated) transaction.reassignAmounts(nChangePosRet); - if(recipients[0].fUseInstantSend && newTx->GetValueOut() > sporkManager.GetSporkValue(SPORK_5_INSTANTSEND_MAX_VALUE)*COIN){ - Q_EMIT message(tr("Send Coins"), tr("InstantSend doesn't support sending values that high yet. Transactions are currently limited to %1 DASH.").arg(sporkManager.GetSporkValue(SPORK_5_INSTANTSEND_MAX_VALUE)), - CClientUIInterface::MSG_ERROR); - return TransactionCreationFailed; + if(recipients[0].fUseInstantSend) { + if(newTx->GetValueOut() > sporkManager.GetSporkValue(SPORK_5_INSTANTSEND_MAX_VALUE)*COIN) { + Q_EMIT message(tr("Send Coins"), tr("InstantSend doesn't support sending values that high yet. Transactions are currently limited to %1 DASH.").arg(sporkManager.GetSporkValue(SPORK_5_INSTANTSEND_MAX_VALUE)), + CClientUIInterface::MSG_ERROR); + return TransactionCreationFailed; + } + if(newTx->vin.size() > CTxLockRequest::MAX_INPUTS) { + Q_EMIT message(tr("Send Coins"), tr("InstantSend doesn't support transactions with more than %1 inputs.").arg(CTxLockRequest::MAX_INPUTS), + CClientUIInterface::MSG_ERROR); + return TransactionCreationFailed; + } } if(!fCreated) diff --git a/src/rpcrawtransaction.cpp b/src/rpcrawtransaction.cpp index fdca27903..79b001815 100644 --- a/src/rpcrawtransaction.cpp +++ b/src/rpcrawtransaction.cpp @@ -871,12 +871,8 @@ UniValue sendrawtransaction(const UniValue& params, bool fHelp) } else if (fHaveChain) { throw JSONRPCError(RPC_TRANSACTION_ALREADY_IN_CHAIN, "transaction already in block chain"); } - if (fInstantSend) { - if (!IsInstantSendTxValid(tx)) { - throw JSONRPCError(RPC_TRANSACTION_ERROR, "Not a valid InstantSend transaction"); - } - mapLockRequestAccepted.insert(make_pair(hashTx, tx)); - CreateTxLockCandidate(tx); + if (fInstantSend && !instantsend.ProcessTxLockRequest(tx)) { + throw JSONRPCError(RPC_TRANSACTION_ERROR, "Not a valid InstantSend transaction, see debug.log for more info"); } RelayTransaction(tx); diff --git a/src/version.h b/src/version.h index 67902f6c7..10fee6e3d 100644 --- a/src/version.h +++ b/src/version.h @@ -10,7 +10,7 @@ * network protocol versioning */ -static const int PROTOCOL_VERSION = 70204; +static const int PROTOCOL_VERSION = 70205; //! initial proto version, to be increased after version/verack negotiation static const int INIT_PROTO_VERSION = 209; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 5c3568284..e0e2fd772 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1472,9 +1472,8 @@ bool CWalletTx::RelayWalletTransaction(std::string strCommand) uint256 hash = GetHash(); LogPrintf("Relaying wtx %s\n", hash.ToString()); - if(strCommand == NetMsgType::TXLOCKREQUEST){ - mapLockRequestAccepted.insert(make_pair(hash, (CTransaction)*this)); - CreateTxLockCandidate(((CTransaction)*this)); + if(strCommand == NetMsgType::TXLOCKREQUEST) { + instantsend.ProcessTxLockRequest(((CTxLockRequest)*this)); } RelayTransaction((CTransaction)*this); return true; @@ -2098,8 +2097,8 @@ void CWallet::AvailableCoins(vector& vCoins, bool fOnlyConfirmed, const continue; int nDepth = pcoin->GetDepthInMainChain(false); - // do not use IX for inputs that have less then 6 blockchain confirmations - if (fUseInstantSend && nDepth < 6) + // do not use IX for inputs that have less then INSTANTSEND_CONFIRMATIONS_REQUIRED blockchain confirmations + if (fUseInstantSend && nDepth < INSTANTSEND_CONFIRMATIONS_REQUIRED) continue; // We should not consider coins which aren't at least in our mempool @@ -2358,6 +2357,10 @@ bool CWallet::SelectCoins(const CAmount& nTargetValue, setvout[out.i].nValue; setCoinsRet.insert(make_pair(out.tx, out.i)); } + + if(fUseInstantSend && setCoinsRet.size() > CTxLockRequest::MAX_INPUTS) + return false; + return (nValueRet >= nTargetValue); } @@ -2885,7 +2888,7 @@ bool CWallet::ConvertList(std::vector vecTxIn, std::vector& vecA bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosRet, std::string& strFailReason, const CCoinControl* coinControl, bool sign, AvailableCoinsType nCoinType, bool fUseInstantSend) { - CAmount nFeePay = fUseInstantSend ? INSTANTSEND_MIN_FEE : 0; + CAmount nFeePay = fUseInstantSend ? CTxLockRequest().GetMinFee() : 0; CAmount nValue = 0; unsigned int nSubtractFeeFromAmount = 0; @@ -3000,19 +3003,26 @@ bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wt if (!SelectCoins(nValueToSelect, setCoins, nValueIn, coinControl, nCoinType, fUseInstantSend)) { - if (nCoinType == ALL_COINS) { - strFailReason = _("Insufficient funds."); - } else if (nCoinType == ONLY_NOT1000IFMN) { + if (nCoinType == ONLY_NOT1000IFMN) { strFailReason = _("Unable to locate enough funds for this transaction that are not equal 1000 DASH."); } else if (nCoinType == ONLY_NONDENOMINATED_NOT1000IFMN) { strFailReason = _("Unable to locate enough PrivateSend non-denominated funds for this transaction that are not equal 1000 DASH."); - } else { + } else if (nCoinType == ONLY_DENOMINATED) { strFailReason = _("Unable to locate enough PrivateSend denominated funds for this transaction."); strFailReason += " " + _("PrivateSend uses exact denominated amounts to send funds, you might simply need to anonymize some more coins."); + } else if (nValueIn < nValueToSelect) { + strFailReason = _("Insufficient funds."); } - - if(fUseInstantSend){ - strFailReason += " " + _("InstantSend requires inputs with at least 6 confirmations, you might need to wait a few minutes and try again."); + if (fUseInstantSend) { + size_t nMaxInputs = CTxLockRequest::MAX_INPUTS; + if(setCoins.size() > nMaxInputs) { + strFailReason += " " + strprintf(_("InstantSend doesn't support transactions with more than %d inputs."), nMaxInputs); + } else if (nValueIn > sporkManager.GetSporkValue(SPORK_5_INSTANTSEND_MAX_VALUE)*COIN) { + strFailReason += " " + strprintf(_("InstantSend doesn't support sending values that high yet. Transactions are currently limited to %1 DASH."), sporkManager.GetSporkValue(SPORK_5_INSTANTSEND_MAX_VALUE)); + } else { + // could be not true but most likely that's the reason + strFailReason += " " + strprintf(_("InstantSend requires inputs with at least %d confirmations, you might need to wait a few minutes and try again."), INSTANTSEND_CONFIRMATIONS_REQUIRED); + } } return false; @@ -3205,6 +3215,9 @@ bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wt if (coinControl && nFeeNeeded > 0 && coinControl->nMinimumTotalFee > nFeeNeeded) { nFeeNeeded = coinControl->nMinimumTotalFee; } + if(fUseInstantSend) { + nFeeNeeded = std::max(nFeeNeeded, CTxLockRequest(txNew).GetMinFee()); + } // If we made it here and we aren't even able to meet the relay fee on the next pass, give up // because we must be at the maximum allowed fee. @@ -4045,7 +4058,7 @@ int CMerkleTx::GetDepthInMainChain(const CBlockIndex* &pindexRet, bool enableIX) } } - if(enableIX && nResult < 6 && IsLockedInstandSendTransaction(GetHash())) + if(enableIX && nResult < 6 && instantsend.IsLockedInstantSendTransaction(GetHash())) return nInstantSendDepth + nResult; return nResult;