diff --git a/qa/rpc-tests/autoix-mempool.py b/qa/rpc-tests/autoix-mempool.py index fdd3a57f3d..1d5e6ff90e 100755 --- a/qa/rpc-tests/autoix-mempool.py +++ b/qa/rpc-tests/autoix-mempool.py @@ -122,6 +122,21 @@ class AutoIXMempoolTest(DashTestFramework): def run_test(self): # make sure masternodes are synced sync_masternodes(self.nodes) + + self.nodes[0].spork("SPORK_17_QUORUM_DKG_ENABLED", 0) + self.wait_for_sporks_same() + self.mine_quorum() + + print("Test old InstantSend") + self.test_auto(); + + self.nodes[0].spork("SPORK_20_INSTANTSEND_LLMQ_BASED", 0) + self.wait_for_sporks_same() + + print("Test new InstantSend") + self.test_auto(True); + + def test_auto(self, new_is = False): self.activate_autoix_bip9() self.set_autoix_spork_state(True) @@ -146,13 +161,17 @@ class AutoIXMempoolTest(DashTestFramework): # fill mempool with transactions self.set_autoix_spork_state(False) + self.nodes[0].spork("SPORK_2_INSTANTSEND_ENABLED", 4070908800) + self.wait_for_sporks_same() self.fill_mempool() self.set_autoix_spork_state(True) + self.nodes[0].spork("SPORK_2_INSTANTSEND_ENABLED", 0) + self.wait_for_sporks_same() # autoIX is not working now assert(not self.send_simple_tx(sender, receiver)) - # regular IX is still working - assert(self.send_regular_IX(sender, receiver)) + # regular IX is still working for old IS but not for new one + assert(not self.send_regular_IX(sender, receiver) if new_is else self.send_regular_IX(sender, receiver)) # generate one block to clean up mempool and retry auto and regular IX # generate 2 more blocks to have enough confirmations for IX diff --git a/qa/rpc-tests/p2p-autoinstantsend.py b/qa/rpc-tests/p2p-autoinstantsend.py index b19eb07a4f..bf930b921a 100755 --- a/qa/rpc-tests/p2p-autoinstantsend.py +++ b/qa/rpc-tests/p2p-autoinstantsend.py @@ -89,13 +89,14 @@ class AutoInstantSendTest(DashTestFramework): self.nodes[0].spork('SPORK_16_INSTANTSEND_AUTOLOCKS', value) # sends regular IX with high fee and may inputs (not-simple transaction) - def send_regular_IX(self): + def send_regular_IX(self, check_fee = True): receiver_addr = self.nodes[self.receiver_idx].getnewaddress() txid = self.nodes[0].instantsendtoaddress(receiver_addr, 1.0) - MIN_FEE = satoshi_round(-0.0001) - fee = self.nodes[0].gettransaction(txid)['fee'] - expected_fee = MIN_FEE * len(self.nodes[0].getrawtransaction(txid, True)['vin']) - assert_equal(fee, expected_fee) + if (check_fee): + MIN_FEE = satoshi_round(-0.0001) + fee = self.nodes[0].gettransaction(txid)['fee'] + expected_fee = MIN_FEE * len(self.nodes[0].getrawtransaction(txid, True)['vin']) + assert_equal(fee, expected_fee) return self.wait_for_instantlock(txid, self.nodes[0]) # sends simple trx, it should become IX if autolocks are allowed @@ -115,6 +116,21 @@ class AutoInstantSendTest(DashTestFramework): def run_test(self): # make sure masternodes are synced sync_masternodes(self.nodes) + + self.nodes[0].spork("SPORK_17_QUORUM_DKG_ENABLED", 0) + self.wait_for_sporks_same() + self.mine_quorum() + + print("Test old InstantSend") + self.test_auto(); + + self.nodes[0].spork("SPORK_20_INSTANTSEND_LLMQ_BASED", 0) + self.wait_for_sporks_same() + + print("Test new InstantSend") + self.test_auto(True); + + def test_auto(self, new_is = False): # feed the sender with some balance sender_addr = self.nodes[self.sender_idx].getnewaddress() self.nodes[0].sendtoaddress(sender_addr, 1) @@ -126,9 +142,9 @@ class AutoInstantSendTest(DashTestFramework): assert(not self.get_autoix_spork_state()) - assert(self.send_regular_IX()) - assert(not self.send_simple_tx()) - assert(not self.send_complex_tx()) + assert(self.send_regular_IX(not new_is)) + assert(self.send_simple_tx() if new_is else not self.send_simple_tx()) + assert(self.send_complex_tx() if new_is else not self.send_complex_tx()) self.activate_autoix_bip9() self.set_autoix_spork_state(True) @@ -136,16 +152,16 @@ class AutoInstantSendTest(DashTestFramework): assert(self.get_autoix_bip9_status() == 'active') assert(self.get_autoix_spork_state()) - assert(self.send_regular_IX()) + assert(self.send_regular_IX(not new_is)) assert(self.send_simple_tx()) - assert(not self.send_complex_tx()) + assert(self.send_complex_tx() if new_is else not self.send_complex_tx()) self.set_autoix_spork_state(False) assert(not self.get_autoix_spork_state()) - assert(self.send_regular_IX()) - assert(not self.send_simple_tx()) - assert(not self.send_complex_tx()) + assert(self.send_regular_IX(not new_is)) + assert(self.send_simple_tx() if new_is else not self.send_simple_tx()) + assert(self.send_complex_tx() if new_is else not self.send_complex_tx()) if __name__ == '__main__': diff --git a/qa/rpc-tests/p2p-instantsend.py b/qa/rpc-tests/p2p-instantsend.py index 9a1b15b6b0..9653919196 100755 --- a/qa/rpc-tests/p2p-instantsend.py +++ b/qa/rpc-tests/p2p-instantsend.py @@ -21,6 +21,20 @@ class InstantSendTest(DashTestFramework): self.sender_idx = self.num_nodes - 3 def run_test(self): + self.nodes[0].spork("SPORK_17_QUORUM_DKG_ENABLED", 0) + self.wait_for_sporks_same() + self.mine_quorum() + + print("Test old InstantSend") + self.test_doublespend() + + self.nodes[0].spork("SPORK_20_INSTANTSEND_LLMQ_BASED", 0) + self.wait_for_sporks_same() + + print("Test new InstantSend") + self.test_doublespend() + + def test_doublespend(self): # feed the sender with some balance sender_addr = self.nodes[self.sender_idx].getnewaddress() self.nodes[0].sendtoaddress(sender_addr, 1) @@ -73,7 +87,12 @@ class InstantSendTest(DashTestFramework): assert (res['hash'] != wrong_block) # wait for long time only for first node timeout = 1 - + # mine more blocks + # TODO: mine these blocks on an isolated node + set_mocktime(get_mocktime() + 1) + set_node_times(self.nodes, get_mocktime()) + self.nodes[0].generate(2) + self.sync_all() if __name__ == '__main__': InstantSendTest().main() diff --git a/qa/rpc-tests/test_framework/test_framework.py b/qa/rpc-tests/test_framework/test_framework.py index b6f354f138..e0d5fbdd84 100755 --- a/qa/rpc-tests/test_framework/test_framework.py +++ b/qa/rpc-tests/test_framework/test_framework.py @@ -498,36 +498,44 @@ class DashTestFramework(BitcoinTestFramework): set_mocktime(get_mocktime() + 1) set_node_times(self.nodes, get_mocktime()) self.nodes[0].generate(skip_count) + sync_blocks(self.nodes) # Make sure all reached phase 1 (init) self.wait_for_quorum_phase(1, None, 0) + # Give nodes some time to connect to neighbors + sleep(2) set_mocktime(get_mocktime() + 1) set_node_times(self.nodes, get_mocktime()) self.nodes[0].generate(2) + sync_blocks(self.nodes) # Make sure all reached phase 2 (contribute) and received all contributions self.wait_for_quorum_phase(2, "receivedContributions", expected_valid_count) set_mocktime(get_mocktime() + 1) set_node_times(self.nodes, get_mocktime()) self.nodes[0].generate(2) + sync_blocks(self.nodes) # Make sure all reached phase 3 (complain) and received all complaints self.wait_for_quorum_phase(3, "receivedComplaints" if expected_valid_count != 10 else None, expected_valid_count) set_mocktime(get_mocktime() + 1) set_node_times(self.nodes, get_mocktime()) self.nodes[0].generate(2) + sync_blocks(self.nodes) # Make sure all reached phase 4 (justify) self.wait_for_quorum_phase(4, None, 0) set_mocktime(get_mocktime() + 1) set_node_times(self.nodes, get_mocktime()) self.nodes[0].generate(2) + sync_blocks(self.nodes) # Make sure all reached phase 5 (commit) self.wait_for_quorum_phase(5, "receivedPrematureCommitments", expected_valid_count) set_mocktime(get_mocktime() + 1) set_node_times(self.nodes, get_mocktime()) self.nodes[0].generate(2) + sync_blocks(self.nodes) # Make sure all reached phase 6 (mining) self.wait_for_quorum_phase(6, None, 0) @@ -544,6 +552,7 @@ class DashTestFramework(BitcoinTestFramework): set_mocktime(get_mocktime() + 1) set_node_times(self.nodes, get_mocktime()) self.nodes[0].generate(1) + sync_blocks(self.nodes) sync_blocks(self.nodes) diff --git a/src/Makefile.am b/src/Makefile.am index 04a5efa3a8..e87247f8b4 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -173,6 +173,7 @@ BITCOIN_CORE_H = \ llmq/quorums_dkgsessionmgr.h \ llmq/quorums_dkgsession.h \ llmq/quorums_init.h \ + llmq/quorums_instantsend.h \ llmq/quorums_signing.h \ llmq/quorums_signing_shares.h \ llmq/quorums_utils.h \ @@ -291,6 +292,7 @@ libdash_server_a_SOURCES = \ llmq/quorums_dkgsessionmgr.cpp \ llmq/quorums_dkgsession.cpp \ llmq/quorums_init.cpp \ + llmq/quorums_instantsend.cpp \ llmq/quorums_signing.cpp \ llmq/quorums_signing_shares.cpp \ llmq/quorums_utils.cpp \ diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 2cc5aafb2f..8d850068c2 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -308,6 +308,7 @@ public: consensus.llmqs[Consensus::LLMQ_400_60] = llmq400_60; consensus.llmqs[Consensus::LLMQ_400_85] = llmq400_85; consensus.llmqChainLocks = Consensus::LLMQ_400_60; + consensus.llmqForInstantSend = Consensus::LLMQ_50_60; fMiningRequiresPeers = true; fDefaultConsistencyChecks = false; @@ -476,6 +477,7 @@ public: consensus.llmqs[Consensus::LLMQ_400_60] = llmq400_60; consensus.llmqs[Consensus::LLMQ_400_85] = llmq400_85; consensus.llmqChainLocks = Consensus::LLMQ_50_60; + consensus.llmqForInstantSend = Consensus::LLMQ_50_60; fMiningRequiresPeers = true; fDefaultConsistencyChecks = false; @@ -623,6 +625,7 @@ public: consensus.llmqs[Consensus::LLMQ_400_60] = llmq400_60; consensus.llmqs[Consensus::LLMQ_400_85] = llmq400_85; consensus.llmqChainLocks = Consensus::LLMQ_50_60; + consensus.llmqForInstantSend = Consensus::LLMQ_50_60; fMiningRequiresPeers = true; fDefaultConsistencyChecks = false; @@ -787,6 +790,7 @@ public: consensus.llmqs[Consensus::LLMQ_10_60] = llmq10_60; consensus.llmqs[Consensus::LLMQ_50_60] = llmq50_60; consensus.llmqChainLocks = Consensus::LLMQ_10_60; + consensus.llmqForInstantSend = Consensus::LLMQ_10_60; } void UpdateBIP9Parameters(Consensus::DeploymentPos d, int64_t nStartTime, int64_t nTimeout, int64_t nWindowSize, int64_t nThreshold) diff --git a/src/consensus/params.h b/src/consensus/params.h index 083cbacfa2..b2efd4ca75 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -174,6 +174,7 @@ struct Params { std::map llmqs; LLMQType llmqChainLocks; + LLMQType llmqForInstantSend{LLMQ_NONE}; }; } // namespace Consensus diff --git a/src/dsnotificationinterface.cpp b/src/dsnotificationinterface.cpp index 9eeac0218e..418f1f9e28 100644 --- a/src/dsnotificationinterface.cpp +++ b/src/dsnotificationinterface.cpp @@ -18,6 +18,7 @@ #include "llmq/quorums.h" #include "llmq/quorums_chainlocks.h" +#include "llmq/quorums_instantsend.h" #include "llmq/quorums_dkgsessionmgr.h" void CDSNotificationInterface::InitializeCurrentBlockTip() @@ -70,8 +71,15 @@ void CDSNotificationInterface::UpdatedBlockTip(const CBlockIndex *pindexNew, con llmq::quorumDKGSessionManager->UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); } +void CDSNotificationInterface::NewPoWValidBlock(const CBlockIndex* pindex, const std::shared_ptr& block) +{ + llmq::chainLocksHandler->NewPoWValidBlock(pindex, block); +} + void CDSNotificationInterface::SyncTransaction(const CTransaction &tx, const CBlockIndex *pindex, int posInBlock) { + llmq::quorumInstantSendManager->SyncTransaction(tx, pindex, posInBlock); + llmq::chainLocksHandler->SyncTransaction(tx, pindex, posInBlock); instantsend.SyncTransaction(tx, pindex, posInBlock); CPrivateSend::SyncTransaction(tx, pindex, posInBlock); } @@ -82,3 +90,8 @@ void CDSNotificationInterface::NotifyMasternodeListChanged(const CDeterministicM governance.CheckMasternodeOrphanVotes(connman); governance.UpdateCachesAndClean(); } + +void CDSNotificationInterface::NotifyChainLock(const CBlockIndex* pindex) +{ + llmq::quorumInstantSendManager->NotifyChainLock(pindex); +} diff --git a/src/dsnotificationinterface.h b/src/dsnotificationinterface.h index d800289d12..c345551153 100644 --- a/src/dsnotificationinterface.h +++ b/src/dsnotificationinterface.h @@ -21,8 +21,10 @@ protected: void AcceptedBlockHeader(const CBlockIndex *pindexNew) override; void NotifyHeaderTip(const CBlockIndex *pindexNew, bool fInitialDownload) override; void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) override; + void NewPoWValidBlock(const CBlockIndex *pindex, const std::shared_ptr& block) override; void SyncTransaction(const CTransaction &tx, const CBlockIndex *pindex, int posInBlock) override; void NotifyMasternodeListChanged(const CDeterministicMNList& newList) override; + void NotifyChainLock(const CBlockIndex* pindex) override; private: CConnman& connman; diff --git a/src/governance-object.cpp b/src/governance-object.cpp index 902138fd40..e8c370ade3 100644 --- a/src/governance-object.cpp +++ b/src/governance-object.cpp @@ -16,6 +16,8 @@ #include "util.h" #include "validation.h" +#include "llmq/quorums_instantsend.h" + #include #include @@ -618,7 +620,7 @@ bool CGovernanceObject::IsCollateralValid(std::string& strError, bool& fMissingC } if ((nConfirmationsIn < GOVERNANCE_FEE_CONFIRMATIONS) && - (!instantsend.IsLockedInstantSendTransaction(nCollateralHash))) { + (!instantsend.IsLockedInstantSendTransaction(nCollateralHash) || llmq::quorumInstantSendManager->IsLocked(nCollateralHash))) { strError = strprintf("Collateral requires at least %d confirmations to be relayed throughout the network (it has only %d)", GOVERNANCE_FEE_CONFIRMATIONS, nConfirmationsIn); if (nConfirmationsIn >= GOVERNANCE_MIN_RELAY_FEE_CONFIRMATIONS) { fMissingConfirmations = true; diff --git a/src/instantx.cpp b/src/instantx.cpp index 9096a6707a..87bbd1a5b6 100644 --- a/src/instantx.cpp +++ b/src/instantx.cpp @@ -25,6 +25,8 @@ #include "wallet/wallet.h" #endif // ENABLE_WALLET +#include "llmq/quorums_instantsend.h" + #include #include @@ -56,7 +58,7 @@ const std::string CInstantSend::SERIALIZATION_VERSION_STRING = "CInstantSend-Ver void CInstantSend::ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman) { if (fLiteMode) return; // disable all Dash specific functionality - if (!sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED)) return; + if (!llmq::IsOldInstantSendEnabled()) return; // NOTE: NetMsgType::TXLOCKREQUEST is handled via ProcessMessage() in net_processing.cpp @@ -224,7 +226,7 @@ void CInstantSend::Vote(const uint256& txHash, CConnman& connman) void CInstantSend::Vote(CTxLockCandidate& txLockCandidate, CConnman& connman) { if (!fMasternodeMode) return; - if (!sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED)) return; + if (!llmq::IsOldInstantSendEnabled()) return; AssertLockHeld(cs_main); AssertLockHeld(cs_instantsend); @@ -496,7 +498,7 @@ void CInstantSend::ProcessOrphanTxLockVotes() void CInstantSend::TryToFinalizeLockCandidate(const CTxLockCandidate& txLockCandidate) { - if (!sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED)) return; + if (!llmq::IsOldInstantSendEnabled()) return; AssertLockHeld(cs_main); AssertLockHeld(cs_instantsend); @@ -547,7 +549,7 @@ void CInstantSend::UpdateLockedTransaction(const CTxLockCandidate& txLockCandida void CInstantSend::LockTransactionInputs(const CTxLockCandidate& txLockCandidate) { - if (!sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED)) return; + if (!llmq::IsOldInstantSendEnabled()) return; LOCK(cs_instantsend); @@ -740,6 +742,10 @@ void CInstantSend::CheckAndRemove() bool CInstantSend::AlreadyHave(const uint256& hash) { + if (!llmq::IsOldInstantSendEnabled()) { + return true; + } + LOCK(cs_instantsend); return mapLockRequestAccepted.count(hash) || mapLockRequestRejected.count(hash) || @@ -766,6 +772,10 @@ bool CInstantSend::HasTxLockRequest(const uint256& txHash) bool CInstantSend::GetTxLockRequest(const uint256& txHash, CTxLockRequest& txLockRequestRet) { + if (!llmq::IsOldInstantSendEnabled()) { + return false; + } + LOCK(cs_instantsend); std::map::iterator it = mapTxLockCandidates.find(txHash); @@ -777,6 +787,10 @@ bool CInstantSend::GetTxLockRequest(const uint256& txHash, CTxLockRequest& txLoc bool CInstantSend::GetTxLockVote(const uint256& hash, CTxLockVote& txLockVoteRet) { + if (!llmq::IsOldInstantSendEnabled()) { + return false; + } + LOCK(cs_instantsend); std::map::iterator it = mapTxLockVotes.find(hash); @@ -828,7 +842,7 @@ int CInstantSend::GetTransactionLockSignatures(const uint256& txHash) { if (!fEnableInstantSend) return -1; if (GetfLargeWorkForkFound() || GetfLargeWorkInvalidChainFound()) return -2; - if (!sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED)) return -3; + if (!llmq::IsOldInstantSendEnabled()) return -3; LOCK(cs_instantsend); @@ -932,7 +946,7 @@ void CInstantSend::DoMaintenance() bool CInstantSend::CanAutoLock() { - if (!isAutoLockBip9Active) { + if (!isAutoLockBip9Active || !llmq::IsOldInstantSendEnabled()) { return false; } if (!sporkManager.IsSporkActive(SPORK_16_INSTANTSEND_AUTOLOCKS)) { diff --git a/src/instantx.h b/src/instantx.h index c8c0aa5854..aac54e2d1a 100644 --- a/src/instantx.h +++ b/src/instantx.h @@ -45,11 +45,12 @@ extern int nCompleteTXLocks; */ class CInstantSend { -private: - static const std::string SERIALIZATION_VERSION_STRING; +public: /// Automatic locks of "simple" transactions are only allowed /// when mempool usage is lower than this threshold static const double AUTO_IX_MEMPOOL_THRESHOLD; +private: + static const std::string SERIALIZATION_VERSION_STRING; // Keep track of current block height int nCachedBlockHeight; diff --git a/src/llmq/quorums_chainlocks.cpp b/src/llmq/quorums_chainlocks.cpp index 94f251117d..a077224c9e 100644 --- a/src/llmq/quorums_chainlocks.cpp +++ b/src/llmq/quorums_chainlocks.cpp @@ -4,6 +4,7 @@ #include "quorums.h" #include "quorums_chainlocks.h" +#include "quorums_instantsend.h" #include "quorums_signing.h" #include "quorums_utils.h" @@ -11,6 +12,7 @@ #include "net_processing.h" #include "scheduler.h" #include "spork.h" +#include "txmempool.h" #include "validation.h" namespace llmq @@ -34,12 +36,16 @@ CChainLocksHandler::~CChainLocksHandler() { } -void CChainLocksHandler::RegisterAsRecoveredSigsListener() +void CChainLocksHandler::Start() { quorumSigningManager->RegisterRecoveredSigsListener(this); + scheduler->scheduleEvery([&]() { + // regularely retry signing the current chaintip as it might have failed before due to missing ixlocks + TrySignChainTip(); + }, 5000); } -void CChainLocksHandler::UnregisterAsRecoveredSigsListener() +void CChainLocksHandler::Stop() { quorumSigningManager->UnregisterRecoveredSigsListener(this); } @@ -149,6 +155,11 @@ void CChainLocksHandler::ProcessNewChainLock(NodeId from, const llmq::CChainLock LogPrintf("CChainLocksHandler::%s -- processed new CLSIG (%s), peer=%d\n", __func__, clsig.ToString(), from); + + if (lastNotifyChainLockBlockIndex != bestChainLockBlockIndex) { + lastNotifyChainLockBlockIndex = bestChainLockBlockIndex; + GetMainSignals().NotifyChainLock(bestChainLockBlockIndex); + } } void CChainLocksHandler::AcceptedBlockHeader(const CBlockIndex* pindexNew) @@ -178,55 +189,145 @@ void CChainLocksHandler::AcceptedBlockHeader(const CBlockIndex* pindexNew) void CChainLocksHandler::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork) { + // don't call TrySignChainTip directly but instead let the scheduler call it. This way we ensure that cs_main is + // never locked and TrySignChainTip is not called twice in parallel + LOCK(cs); + if (tryLockChainTipScheduled) { + return; + } + tryLockChainTipScheduled = true; + scheduler->scheduleFromNow([&]() { + TrySignChainTip(); + LOCK(cs); + tryLockChainTipScheduled = false; + }, 0); +} + +void CChainLocksHandler::TrySignChainTip() +{ + Cleanup(); + + const CBlockIndex* pindex; + { + LOCK(cs_main); + pindex = chainActive.Tip(); + } + if (!fMasternodeMode) { return; } - if (!pindexNew->pprev) { + if (!pindex->pprev) { return; } if (!sporkManager.IsSporkActive(SPORK_19_CHAINLOCKS_ENABLED)) { return; } - Cleanup(); - // DIP8 defines a process called "Signing attempts" which should run before the CLSIG is finalized // To simplify the initial implementation, we skip this process and directly try to create a CLSIG // This will fail when multiple blocks compete, but we accept this for the initial implementation. // Later, we'll add the multiple attempts process. - uint256 requestId = ::SerializeHash(std::make_pair(CLSIG_REQUESTID_PREFIX, pindexNew->nHeight)); - uint256 msgHash = pindexNew->GetBlockHash(); - { LOCK(cs); - if (bestChainLockBlockIndex == pindexNew) { + if (bestChainLockBlockIndex == pindex) { // we first got the CLSIG, then the header, and then the block was connected. // In this case there is no need to continue here. - return; - } - - if (InternalHasConflictingChainLock(pindexNew->nHeight, pindexNew->GetBlockHash())) { - if (!inEnforceBestChainLock) { - // we accepted this block when there was no lock yet, but now a conflicting lock appeared. Invalidate it. - LogPrintf("CChainLocksHandler::%s -- conflicting lock after block was accepted, invalidating now\n", - __func__); - ScheduleInvalidateBlock(pindexNew); + // However, NotifyChainLock might not have been called yet, so call it now if needed + if (lastNotifyChainLockBlockIndex != bestChainLockBlockIndex) { + lastNotifyChainLockBlockIndex = bestChainLockBlockIndex; + GetMainSignals().NotifyChainLock(bestChainLockBlockIndex); } return; } - if (bestChainLock.nHeight >= pindexNew->nHeight) { + if (pindex->nHeight == lastSignedHeight) { + // already signed this one + return; + } + + if (bestChainLock.nHeight >= pindex->nHeight) { // already got the same CLSIG or a better one return; } - if (pindexNew->nHeight == lastSignedHeight) { - // already signed this one + if (InternalHasConflictingChainLock(pindex->nHeight, pindex->GetBlockHash())) { + if (!inEnforceBestChainLock) { + // we accepted this block when there was no lock yet, but now a conflicting lock appeared. Invalidate it. + LogPrintf("CChainLocksHandler::%s -- conflicting lock after block was accepted, invalidating now\n", + __func__); + ScheduleInvalidateBlock(pindex); + } return; } - lastSignedHeight = pindexNew->nHeight; + } + + LogPrintf("CChainLocksHandler::%s -- trying to sign %s, height=%d\n", __func__, pindex->GetBlockHash().ToString(), pindex->nHeight); + + // When the new IX system is activated, we only try to ChainLock blocks which include safe transactions. A TX is + // considered safe when it is ixlocked or at least known since 10 minutes (from mempool or block). These checks are + // performed for the tip (which we try to sign) and the previous 5 blocks. If a ChainLocked block is found on the + // way down, we consider all TXs to be safe. + if (IsNewInstantSendEnabled() && sporkManager.IsSporkActive(SPORK_3_INSTANTSEND_BLOCK_FILTERING)) { + auto pindexWalk = pindex; + while (pindexWalk) { + if (pindex->nHeight - pindexWalk->nHeight > 5) { + // no need to check further down, 6 confs is safe to assume that TXs below this height won't be + // ixlocked anymore if they aren't already + LogPrintf("CChainLocksHandler::%s -- tip and previous 5 blocks all safe\n", __func__); + break; + } + if (HasChainLock(pindexWalk->nHeight, pindexWalk->GetBlockHash())) { + // we don't care about ixlocks for TXs that are ChainLocked already + LogPrintf("CChainLocksHandler::%s -- chainlock at height %d \n", __func__, pindexWalk->nHeight); + break; + } + + decltype(blockTxs.begin()->second) txids; + { + LOCK(cs); + auto it = blockTxs.find(pindexWalk->GetBlockHash()); + if (it == blockTxs.end()) { + // this should actually not happen as NewPoWValidBlock should have been called before + LogPrintf("CChainLocksHandler::%s -- blockTxs for %s not found\n", __func__, + pindexWalk->GetBlockHash().ToString()); + return; + } + txids = it->second; + } + + for (auto& txid : *txids) { + int64_t txAge = 0; + { + LOCK(cs); + auto it = txFirstSeenTime.find(txid); + if (it != txFirstSeenTime.end()) { + txAge = GetAdjustedTime() - it->second; + } + } + + if (txAge < WAIT_FOR_ISLOCK_TIMEOUT && !quorumInstantSendManager->IsLocked(txid)) { + LogPrintf("CChainLocksHandler::%s -- not signing block %s due to TX %s not being ixlocked and not old enough. age=%d\n", __func__, + pindexWalk->GetBlockHash().ToString(), txid.ToString(), txAge); + return; + } + } + + pindexWalk = pindexWalk->pprev; + } + } + + uint256 requestId = ::SerializeHash(std::make_pair(CLSIG_REQUESTID_PREFIX, pindex->nHeight)); + uint256 msgHash = pindex->GetBlockHash(); + + { + LOCK(cs); + if (bestChainLock.nHeight >= pindex->nHeight) { + // might have happened while we didn't hold cs + return; + } + lastSignedHeight = pindex->nHeight; lastSignedRequestId = requestId; lastSignedMsgHash = msgHash; } @@ -234,6 +335,73 @@ void CChainLocksHandler::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBl quorumSigningManager->AsyncSignIfMember(Params().GetConsensus().llmqChainLocks, requestId, msgHash); } +void CChainLocksHandler::NewPoWValidBlock(const CBlockIndex* pindex, const std::shared_ptr& block) +{ + LOCK(cs); + if (blockTxs.count(pindex->GetBlockHash())) { + // should actually not happen (blocks are only written once to disk and this is when NewPoWValidBlock is called) + // but be extra safe here in case this behaviour changes. + return; + } + + int64_t curTime = GetAdjustedTime(); + + // We listen for NewPoWValidBlock so that we can collect all TX ids of all included TXs of newly received blocks + // We need this information later when we try to sign a new tip, so that we can determine if all included TXs are + // safe. + + auto txs = std::make_shared>(); + for (const auto& tx : block->vtx) { + if (tx->nVersion == 3) { + if (tx->nType == TRANSACTION_COINBASE || + tx->nType == TRANSACTION_QUORUM_COMMITMENT) { + continue; + } + } + txs->emplace(tx->GetHash()); + txFirstSeenTime.emplace(tx->GetHash(), curTime); + } + blockTxs[pindex->GetBlockHash()] = txs; +} + +void CChainLocksHandler::SyncTransaction(const CTransaction& tx, const CBlockIndex* pindex, int posInBlock) +{ + if (tx.nVersion == 3) { + if (tx.nType == TRANSACTION_COINBASE || + tx.nType == TRANSACTION_QUORUM_COMMITMENT) { + return; + } + } + + LOCK(cs); + int64_t curTime = GetAdjustedTime(); + txFirstSeenTime.emplace(tx.GetHash(), curTime); +} + +bool CChainLocksHandler::IsTxSafeForMining(const uint256& txid) +{ + if (!sporkManager.IsSporkActive(SPORK_19_CHAINLOCKS_ENABLED) || !sporkManager.IsSporkActive(SPORK_3_INSTANTSEND_BLOCK_FILTERING)) { + return true; + } + if (!IsNewInstantSendEnabled()) { + return true; + } + + int64_t txAge = 0; + { + LOCK(cs); + auto it = txFirstSeenTime.find(txid); + if (it != txFirstSeenTime.end()) { + txAge = GetAdjustedTime() - it->second; + } + } + + if (txAge < WAIT_FOR_ISLOCK_TIMEOUT && !quorumInstantSendManager->IsLocked(txid)) { + return false; + } + return true; +} + // WARNING: cs_main and cs should not be held! void CChainLocksHandler::EnforceBestChainLock() { @@ -410,7 +578,9 @@ void CChainLocksHandler::Cleanup() } } - LOCK2(cs_main, cs); + // need mempool.cs due to GetTransaction calls + LOCK2(cs_main, mempool.cs); + LOCK(cs); for (auto it = seenChainLocks.begin(); it != seenChainLocks.end(); ) { if (GetTimeMillis() - it->second >= CLEANUP_SEEN_TIMEOUT) { @@ -420,6 +590,38 @@ void CChainLocksHandler::Cleanup() } } + for (auto it = blockTxs.begin(); it != blockTxs.end(); ) { + auto pindex = mapBlockIndex.at(it->first); + if (InternalHasChainLock(pindex->nHeight, pindex->GetBlockHash())) { + for (auto& txid : *it->second) { + txFirstSeenTime.erase(txid); + } + it = blockTxs.erase(it); + } else if (InternalHasConflictingChainLock(pindex->nHeight, pindex->GetBlockHash())) { + it = blockTxs.erase(it); + } else { + ++it; + } + } + for (auto it = txFirstSeenTime.begin(); it != txFirstSeenTime.end(); ) { + CTransactionRef tx; + uint256 hashBlock; + if (!GetTransaction(it->first, tx, Params().GetConsensus(), hashBlock)) { + // tx has vanished, probably due to conflicts + it = txFirstSeenTime.erase(it); + } else if (!hashBlock.IsNull()) { + auto pindex = mapBlockIndex.at(hashBlock); + if (chainActive.Tip()->GetAncestor(pindex->nHeight) == pindex && chainActive.Height() - pindex->nHeight >= 6) { + // tx got confirmed >= 6 times, so we can stop keeping track of it + it = txFirstSeenTime.erase(it); + } else { + ++it; + } + } else { + ++it; + } + } + lastCleanupTime = GetTimeMillis(); } diff --git a/src/llmq/quorums_chainlocks.h b/src/llmq/quorums_chainlocks.h index 31065a478f..722621fc65 100644 --- a/src/llmq/quorums_chainlocks.h +++ b/src/llmq/quorums_chainlocks.h @@ -12,6 +12,7 @@ #include "chainparams.h" #include +#include class CBlockIndex; class CScheduler; @@ -45,9 +46,13 @@ class CChainLocksHandler : public CRecoveredSigsListener static const int64_t CLEANUP_INTERVAL = 1000 * 30; static const int64_t CLEANUP_SEEN_TIMEOUT = 24 * 60 * 60 * 1000; + // how long to wait for ixlocks until we consider a block with non-ixlocked TXs to be safe to sign + static const int64_t WAIT_FOR_ISLOCK_TIMEOUT = 10 * 60; + private: CScheduler* scheduler; CCriticalSection cs; + bool tryLockChainTipScheduled{false}; std::atomic inEnforceBestChainLock{false}; uint256 bestChainLockHash; @@ -55,11 +60,16 @@ private: CChainLockSig bestChainLockWithKnownBlock; const CBlockIndex* bestChainLockBlockIndex{nullptr}; + const CBlockIndex* lastNotifyChainLockBlockIndex{nullptr}; int32_t lastSignedHeight{-1}; uint256 lastSignedRequestId; uint256 lastSignedMsgHash; + // We keep track of txids from recently received blocks so that we can check if all TXs got ixlocked + std::unordered_map>> blockTxs; + std::unordered_map txFirstSeenTime; + std::map seenChainLocks; int64_t lastCleanupTime{0}; @@ -68,8 +78,8 @@ public: CChainLocksHandler(CScheduler* _scheduler); ~CChainLocksHandler(); - void RegisterAsRecoveredSigsListener(); - void UnregisterAsRecoveredSigsListener(); + void Start(); + void Stop(); bool AlreadyHave(const CInv& inv); bool GetChainLockByHash(const uint256& hash, CChainLockSig& ret); @@ -78,12 +88,17 @@ public: void ProcessNewChainLock(NodeId from, const CChainLockSig& clsig, const uint256& hash); void AcceptedBlockHeader(const CBlockIndex* pindexNew); void UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork); + void NewPoWValidBlock(const CBlockIndex* pindex, const std::shared_ptr& block); + void SyncTransaction(const CTransaction &tx, const CBlockIndex *pindex, int posInBlock); + void TrySignChainTip(); void EnforceBestChainLock(); virtual void HandleNewRecoveredSig(const CRecoveredSig& recoveredSig); bool HasChainLock(int nHeight, const uint256& blockHash); bool HasConflictingChainLock(int nHeight, const uint256& blockHash); + bool IsTxSafeForMining(const uint256& txid); + private: // these require locks to be held already bool InternalHasChainLock(int nHeight, const uint256& blockHash); diff --git a/src/llmq/quorums_init.cpp b/src/llmq/quorums_init.cpp index ec03b46403..578d9783e4 100644 --- a/src/llmq/quorums_init.cpp +++ b/src/llmq/quorums_init.cpp @@ -10,6 +10,7 @@ #include "quorums_chainlocks.h" #include "quorums_debug.h" #include "quorums_dkgsessionmgr.h" +#include "quorums_instantsend.h" #include "quorums_signing.h" #include "quorums_signing_shares.h" @@ -29,10 +30,13 @@ void InitLLMQSystem(CEvoDB& evoDb, CScheduler* scheduler, bool unitTests) quorumSigSharesManager = new CSigSharesManager(); quorumSigningManager = new CSigningManager(unitTests); chainLocksHandler = new CChainLocksHandler(scheduler); + quorumInstantSendManager = new CInstantSendManager(scheduler); } void DestroyLLMQSystem() { + delete quorumInstantSendManager; + quorumInstantSendManager = nullptr; delete chainLocksHandler; chainLocksHandler = nullptr; delete quorumSigningManager; @@ -62,14 +66,20 @@ void StartLLMQSystem() quorumSigSharesManager->StartWorkerThread(); } if (chainLocksHandler) { - chainLocksHandler->RegisterAsRecoveredSigsListener(); + chainLocksHandler->Start(); + } + if (quorumInstantSendManager) { + quorumInstantSendManager->RegisterAsRecoveredSigsListener(); } } void StopLLMQSystem() { + if (quorumInstantSendManager) { + quorumInstantSendManager->UnregisterAsRecoveredSigsListener(); + } if (chainLocksHandler) { - chainLocksHandler->UnregisterAsRecoveredSigsListener(); + chainLocksHandler->Stop(); } if (quorumSigSharesManager) { quorumSigSharesManager->StopWorkerThread(); diff --git a/src/llmq/quorums_instantsend.cpp b/src/llmq/quorums_instantsend.cpp new file mode 100644 index 0000000000..fec7033e23 --- /dev/null +++ b/src/llmq/quorums_instantsend.cpp @@ -0,0 +1,882 @@ +// Copyright (c) 2019 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "quorums_chainlocks.h" +#include "quorums_instantsend.h" +#include "quorums_utils.h" + +#include "bls/bls_batchverifier.h" +#include "chainparams.h" +#include "coins.h" +#include "txmempool.h" +#include "masternode-sync.h" +#include "net_processing.h" +#include "scheduler.h" +#include "spork.h" +#include "validation.h" + +#ifdef ENABLE_WALLET +#include "wallet/wallet.h" +#endif + +// needed for nCompleteTXLocks +#include "instantx.h" + +#include + +namespace llmq +{ + +static const std::string INPUTLOCK_REQUESTID_PREFIX = "inlock"; +static const std::string ISLOCK_REQUESTID_PREFIX = "islock"; + +CInstantSendManager* quorumInstantSendManager; + +uint256 CInstantSendLock::GetRequestId() const +{ + CHashWriter hw(SER_GETHASH, 0); + hw << ISLOCK_REQUESTID_PREFIX; + hw << inputs; + return hw.GetHash(); +} + +CInstantSendManager::CInstantSendManager(CScheduler* _scheduler) : + scheduler(_scheduler) +{ +} + +CInstantSendManager::~CInstantSendManager() +{ +} + +void CInstantSendManager::RegisterAsRecoveredSigsListener() +{ + quorumSigningManager->RegisterRecoveredSigsListener(this); +} + +void CInstantSendManager::UnregisterAsRecoveredSigsListener() +{ + quorumSigningManager->UnregisterRecoveredSigsListener(this); +} + +bool CInstantSendManager::ProcessTx(CNode* pfrom, const CTransaction& tx, CConnman& connman, const Consensus::Params& params) +{ + if (!IsNewInstantSendEnabled()) { + return true; + } + + auto llmqType = params.llmqForInstantSend; + if (llmqType == Consensus::LLMQ_NONE) { + return true; + } + if (!fMasternodeMode) { + return true; + } + + // Ignore any InstantSend messages until blockchain is synced + if (!masternodeSync.IsBlockchainSynced()) { + return true; + } + + if (IsConflicted(tx)) { + return false; + } + + if (!CheckCanLock(tx, true, params)) { + return false; + } + + std::vector ids; + ids.reserve(tx.vin.size()); + + for (const auto& in : tx.vin) { + auto id = ::SerializeHash(std::make_pair(INPUTLOCK_REQUESTID_PREFIX, in.prevout)); + ids.emplace_back(id); + } + + { + LOCK(cs); + size_t alreadyVotedCount = 0; + for (size_t i = 0; i < ids.size(); i++) { + auto it = inputVotes.find(ids[i]); + if (it != inputVotes.end()) { + if (it->second != tx.GetHash()) { + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: input %s is conflicting with islock %s\n", __func__, + tx.GetHash().ToString(), tx.vin[i].prevout.ToStringShort(), it->second.ToString()); + return false; + } + alreadyVotedCount++; + } + } + if (alreadyVotedCount == ids.size()) { + return true; + } + + for (auto& id : ids) { + inputVotes.emplace(id, tx.GetHash()); + } + } + + // don't even try the actual signing if any input is conflicting + for (auto& id : ids) { + if (quorumSigningManager->IsConflicting(llmqType, id, tx.GetHash())) { + return false; + } + } + for (auto& id : ids) { + quorumSigningManager->AsyncSignIfMember(llmqType, id, tx.GetHash()); + } + + // We might have received all input locks before we got the corresponding TX. In this case, we have to sign the + // islock now instead of waiting for the input locks. + TrySignInstantSendLock(tx); + + return true; +} + +bool CInstantSendManager::CheckCanLock(const CTransaction& tx, bool printDebug, const Consensus::Params& params) +{ + if (sporkManager.IsSporkActive(SPORK_16_INSTANTSEND_AUTOLOCKS) && (mempool.UsedMemoryShare() > CInstantSend::AUTO_IX_MEMPOOL_THRESHOLD)) { + return false; + } + + int nInstantSendConfirmationsRequired = params.nInstantSendConfirmationsRequired; + + uint256 txHash = tx.GetHash(); + CAmount nValueIn = 0; + for (const auto& in : tx.vin) { + CAmount v = 0; + if (!CheckCanLock(in.prevout, printDebug, &txHash, &v, params)) { + return false; + } + + nValueIn += v; + } + + // TODO decide if we should limit max input values. This was ok to do in the old system, but in the new system + // where we want to have all TXs locked at some point, this is counterproductive (especially when ChainLocks later + // depend on all TXs being locked first) +// CAmount maxValueIn = sporkManager.GetSporkValue(SPORK_5_INSTANTSEND_MAX_VALUE); +// if (nValueIn > maxValueIn * COIN) { +// if (printDebug) { +// LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: TX input value too high. nValueIn=%f, maxValueIn=%d", __func__, +// tx.GetHash().ToString(), nValueIn / (double)COIN, maxValueIn); +// } +// return false; +// } + + return true; +} + +bool CInstantSendManager::CheckCanLock(const COutPoint& outpoint, bool printDebug, const uint256* _txHash, CAmount* retValue, const Consensus::Params& params) +{ + int nInstantSendConfirmationsRequired = params.nInstantSendConfirmationsRequired; + + if (IsLocked(outpoint.hash)) { + // if prevout was ix locked, allow locking of descendants (no matter if prevout is in mempool or already mined) + return true; + } + + static uint256 txHashNull; + const uint256* txHash = &txHashNull; + if (_txHash) { + txHash = _txHash; + } + + auto mempoolTx = mempool.get(outpoint.hash); + if (mempoolTx) { + if (printDebug) { + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: parent mempool TX %s is not locked\n", __func__, + txHash->ToString(), outpoint.hash.ToString()); + } + return false; + } + + Coin coin; + const CBlockIndex* pindexMined = nullptr; + { + LOCK(cs_main); + if (!pcoinsTip->GetCoin(outpoint, coin) || coin.IsSpent()) { + if (printDebug) { + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: failed to find UTXO %s\n", __func__, + txHash->ToString(), outpoint.ToStringShort()); + } + return false; + } + pindexMined = chainActive[coin.nHeight]; + } + + int nTxAge = chainActive.Height() - coin.nHeight + 1; + // 1 less than the "send IX" gui requires, in case of a block propagating the network at the time + int nConfirmationsRequired = nInstantSendConfirmationsRequired - 1; + + if (nTxAge < nConfirmationsRequired) { + if (!llmq::chainLocksHandler->HasChainLock(pindexMined->nHeight, pindexMined->GetBlockHash())) { + if (printDebug) { + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: outpoint %s too new and not ChainLocked. nTxAge=%d, nConfirmationsRequired=%d\n", __func__, + txHash->ToString(), outpoint.ToStringShort(), nTxAge, nConfirmationsRequired); + } + return false; + } + } + + if (retValue) { + *retValue = coin.out.nValue; + } + + return true; +} + +void CInstantSendManager::HandleNewRecoveredSig(const CRecoveredSig& recoveredSig) +{ + if (!IsNewInstantSendEnabled()) { + return; + } + + auto llmqType = Params().GetConsensus().llmqForInstantSend; + if (llmqType == Consensus::LLMQ_NONE) { + return; + } + auto& params = Params().GetConsensus().llmqs.at(llmqType); + + uint256 txid; + bool isInstantSendLock = false; + { + LOCK(cs); + auto it = inputVotes.find(recoveredSig.id); + if (it != inputVotes.end()) { + txid = it->second; + } + if (creatingInstantSendLocks.count(recoveredSig.id)) { + isInstantSendLock = true; + } + } + if (!txid.IsNull()) { + HandleNewInputLockRecoveredSig(recoveredSig, txid); + } else if (isInstantSendLock) { + HandleNewInstantSendLockRecoveredSig(recoveredSig); + } +} + +void CInstantSendManager::HandleNewInputLockRecoveredSig(const CRecoveredSig& recoveredSig, const uint256& txid) +{ + auto llmqType = Params().GetConsensus().llmqForInstantSend; + + CTransactionRef tx; + uint256 hashBlock; + if (!GetTransaction(txid, tx, Params().GetConsensus(), hashBlock, true)) { + return; + } + + if (LogAcceptCategory("instantsend")) { + for (auto& in : tx->vin) { + auto id = ::SerializeHash(std::make_pair(INPUTLOCK_REQUESTID_PREFIX, in.prevout)); + if (id == recoveredSig.id) { + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: got recovered sig for input %s\n", __func__, + txid.ToString(), in.prevout.ToStringShort()); + break; + } + } + } + + TrySignInstantSendLock(*tx); +} + +void CInstantSendManager::TrySignInstantSendLock(const CTransaction& tx) +{ + auto llmqType = Params().GetConsensus().llmqForInstantSend; + + for (auto& in : tx.vin) { + auto id = ::SerializeHash(std::make_pair(INPUTLOCK_REQUESTID_PREFIX, in.prevout)); + if (!quorumSigningManager->HasRecoveredSig(llmqType, id, tx.GetHash())) { + return; + } + } + + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: got all recovered sigs, creating CInstantSendLock\n", __func__, + tx.GetHash().ToString()); + + CInstantSendLockInfo islockInfo; + islockInfo.time = GetTimeMillis(); + islockInfo.islock.txid = tx.GetHash(); + for (auto& in : tx.vin) { + islockInfo.islock.inputs.emplace_back(in.prevout); + } + + auto id = islockInfo.islock.GetRequestId(); + + if (quorumSigningManager->HasRecoveredSigForId(llmqType, id)) { + return; + } + + { + LOCK(cs); + auto e = creatingInstantSendLocks.emplace(id, islockInfo); + if (!e.second) { + return; + } + txToCreatingInstantSendLocks.emplace(tx.GetHash(), &e.first->second); + } + + quorumSigningManager->AsyncSignIfMember(llmqType, id, tx.GetHash()); +} + +void CInstantSendManager::HandleNewInstantSendLockRecoveredSig(const llmq::CRecoveredSig& recoveredSig) +{ + CInstantSendLockInfo islockInfo; + + { + LOCK(cs); + auto it = creatingInstantSendLocks.find(recoveredSig.id); + if (it == creatingInstantSendLocks.end()) { + return; + } + + islockInfo = std::move(it->second); + creatingInstantSendLocks.erase(it); + txToCreatingInstantSendLocks.erase(islockInfo.islock.txid); + } + + if (islockInfo.islock.txid != recoveredSig.msgHash) { + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: islock conflicts with %s, dropping own version", __func__, + islockInfo.islock.txid.ToString(), recoveredSig.msgHash.ToString()); + return; + } + + islockInfo.islock.sig = recoveredSig.sig; + ProcessInstantSendLock(-1, ::SerializeHash(islockInfo.islock), islockInfo.islock); +} + +void CInstantSendManager::ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman) +{ + if (!IsNewInstantSendEnabled()) { + return; + } + + if (strCommand == NetMsgType::ISLOCK) { + CInstantSendLock islock; + vRecv >> islock; + ProcessMessageInstantSendLock(pfrom, islock, connman); + } +} + +void CInstantSendManager::ProcessMessageInstantSendLock(CNode* pfrom, const llmq::CInstantSendLock& islock, CConnman& connman) +{ + bool ban = false; + if (!PreVerifyInstantSendLock(pfrom->id, islock, ban)) { + if (ban) { + LOCK(cs_main); + Misbehaving(pfrom->id, 100); + } + return; + } + + auto hash = ::SerializeHash(islock); + + LOCK(cs); + if (pendingInstantSendLocks.count(hash) || finalInstantSendLocks.count(hash)) { + return; + } + + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: received islock, peer=%d\n", __func__, + islock.txid.ToString(), hash.ToString(), pfrom->id); + + pendingInstantSendLocks.emplace(hash, std::make_pair(pfrom->id, std::move(islock))); + + if (!hasScheduledProcessPending) { + hasScheduledProcessPending = true; + scheduler->scheduleFromNow([&] { + ProcessPendingInstantSendLocks(); + }, 100); + } +} + +bool CInstantSendManager::PreVerifyInstantSendLock(NodeId nodeId, const llmq::CInstantSendLock& islock, bool& retBan) +{ + retBan = false; + + if (islock.txid.IsNull() || !islock.sig.IsValid() || islock.inputs.empty()) { + retBan = true; + return false; + } + + std::set dups; + for (auto& o : islock.inputs) { + if (!dups.emplace(o).second) { + retBan = true; + return false; + } + } + + return true; +} + +void CInstantSendManager::ProcessPendingInstantSendLocks() +{ + auto llmqType = Params().GetConsensus().llmqForInstantSend; + + decltype(pendingInstantSendLocks) pend; + + { + LOCK(cs); + hasScheduledProcessPending = false; + pend = std::move(pendingInstantSendLocks); + } + + if (!IsNewInstantSendEnabled()) { + return; + } + + int tipHeight; + { + LOCK(cs_main); + tipHeight = chainActive.Height(); + } + + CBLSBatchVerifier batchVerifier(false, true, 8); + std::unordered_map> recSigs; + + for (const auto& p : pend) { + auto& hash = p.first; + auto nodeId = p.second.first; + auto& islock = p.second.second; + + auto id = islock.GetRequestId(); + + // no need to verify an ISLOCK if we already have verified the recovered sig that belongs to it + if (quorumSigningManager->HasRecoveredSig(llmqType, id, islock.txid)) { + continue; + } + + auto quorum = quorumSigningManager->SelectQuorumForSigning(llmqType, tipHeight, id); + if (!quorum) { + // should not happen, but if one fails to select, all others will also fail to select + return; + } + uint256 signHash = CLLMQUtils::BuildSignHash(llmqType, quorum->quorumHash, id, islock.txid); + batchVerifier.PushMessage(nodeId, hash, signHash, islock.sig, quorum->quorumPublicKey); + + // We can reconstruct the CRecoveredSig objects from the islock and pass it to the signing manager, which + // avoids unnecessary double-verification of the signature. We however only do this when verification here + // turns out to be good (which is checked further down) + if (!quorumSigningManager->HasRecoveredSigForId(llmqType, id)) { + CRecoveredSig recSig; + recSig.llmqType = llmqType; + recSig.quorumHash = quorum->quorumHash; + recSig.id = id; + recSig.msgHash = islock.txid; + recSig.sig = islock.sig; + recSigs.emplace(std::piecewise_construct, + std::forward_as_tuple(hash), + std::forward_as_tuple(std::move(quorum), std::move(recSig))); + } + } + + batchVerifier.Verify(); + + if (!batchVerifier.badSources.empty()) { + LOCK(cs_main); + for (auto& nodeId : batchVerifier.badSources) { + // Let's not be too harsh, as the peer might simply be unlucky and might have sent us an old lock which + // does not validate anymore due to changed quorums + Misbehaving(nodeId, 20); + } + } + for (const auto& p : pend) { + auto& hash = p.first; + auto nodeId = p.second.first; + auto& islock = p.second.second; + + if (batchVerifier.badMessages.count(hash)) { + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: invalid sig in islock, peer=%d\n", __func__, + islock.txid.ToString(), hash.ToString(), nodeId); + continue; + } + + ProcessInstantSendLock(nodeId, hash, islock); + + // See comment further on top. We pass a reconstructed recovered sig to the signing manager to avoid + // double-verification of the sig. + auto it = recSigs.find(hash); + if (it != recSigs.end()) { + auto& quorum = it->second.first; + auto& recSig = it->second.second; + if (!quorumSigningManager->HasRecoveredSigForId(llmqType, recSig.id)) { + recSig.UpdateHash(); + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: passing reconstructed recSig to signing mgr, peer=%d\n", __func__, + islock.txid.ToString(), hash.ToString(), nodeId); + quorumSigningManager->PushReconstructedRecoveredSig(recSig, quorum); + } + } + } +} + +void CInstantSendManager::ProcessInstantSendLock(NodeId from, const uint256& hash, const CInstantSendLock& islock) +{ + { + LOCK(cs_main); + g_connman->RemoveAskFor(hash); + } + + CInstantSendLockInfo islockInfo; + islockInfo.time = GetTimeMillis(); + islockInfo.islock = islock; + islockInfo.islockHash = hash; + + uint256 hashBlock; + // we ignore failure here as we must be able to propagate the lock even if we don't have the TX locally + if (GetTransaction(islock.txid, islockInfo.tx, Params().GetConsensus(), hashBlock)) { + if (!hashBlock.IsNull()) { + { + LOCK(cs_main); + islockInfo.pindexMined = mapBlockIndex.at(hashBlock); + } + + // Let's see if the TX that was locked by this islock is already mined in a ChainLocked block. If yes, + // we can simply ignore the islock, as the ChainLock implies locking of all TXs in that chain + if (llmq::chainLocksHandler->HasChainLock(islockInfo.pindexMined->nHeight, islockInfo.pindexMined->GetBlockHash())) { + LogPrint("instantsend", "CInstantSendManager::%s -- txlock=%s, islock=%s: dropping islock as it already got a ChainLock in block %s, peer=%d\n", __func__, + islock.txid.ToString(), hash.ToString(), hashBlock.ToString(), from); + return; + } + } + } + + { + LOCK(cs); + auto e = finalInstantSendLocks.emplace(hash, islockInfo); + if (!e.second) { + return; + } + auto islockInfoPtr = &e.first->second; + + creatingInstantSendLocks.erase(islockInfoPtr->islock.GetRequestId()); + txToCreatingInstantSendLocks.erase(islockInfoPtr->islock.txid); + + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: processsing islock, peer=%d\n", __func__, + islock.txid.ToString(), hash.ToString(), from); + + if (!txToInstantSendLock.emplace(islock.txid, islockInfoPtr).second) { + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: duplicate islock, other islock=%s, peer=%d\n", __func__, + islock.txid.ToString(), hash.ToString(), txToInstantSendLock[islock.txid]->islockHash.ToString(), from); + txToInstantSendLock.erase(hash); + return; + } + for (size_t i = 0; i < islock.inputs.size(); i++) { + auto& in = islock.inputs[i]; + if (!inputToInstantSendLock.emplace(in, islockInfoPtr).second) { + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: conflicting input in islock. input=%s, other islock=%s, peer=%d\n", __func__, + islock.txid.ToString(), hash.ToString(), in.ToStringShort(), inputToInstantSendLock[in]->islockHash.ToString(), from); + txToInstantSendLock.erase(hash); + for (size_t j = 0; j < i; j++) { + inputToInstantSendLock.erase(islock.inputs[j]); + } + return; + } + } + } + + CInv inv(MSG_ISLOCK, hash); + g_connman->RelayInv(inv); + + RemoveMempoolConflictsForLock(hash, islock); + RetryLockMempoolTxs(islock.txid); + + UpdateWalletTransaction(islock.txid); +} + +void CInstantSendManager::UpdateWalletTransaction(const uint256& txid) +{ +#ifdef ENABLE_WALLET + if (!pwalletMain) { + return; + } + + if (pwalletMain->UpdatedTransaction(txid)) { + // bumping this to update UI + nCompleteTXLocks++; + // notify an external script once threshold is reached + std::string strCmd = GetArg("-instantsendnotify", ""); + if (!strCmd.empty()) { + boost::replace_all(strCmd, "%s", txid.GetHex()); + boost::thread t(runCommand, strCmd); // thread runs free + } + } +#endif + + LOCK(cs); + auto it = txToInstantSendLock.find(txid); + if (it == txToInstantSendLock.end()) { + return; + } + if (it->second->tx == nullptr) { + return; + } + + GetMainSignals().NotifyTransactionLock(*it->second->tx); +} + +void CInstantSendManager::SyncTransaction(const CTransaction& tx, const CBlockIndex* pindex, int posInBlock) +{ + if (!IsNewInstantSendEnabled()) { + return; + } + + { + LOCK(cs); + auto it = txToInstantSendLock.find(tx.GetHash()); + if (it == txToInstantSendLock.end()) { + return; + } + auto islockInfo = it->second; + if (islockInfo->tx == nullptr) { + islockInfo->tx = MakeTransactionRef(tx); + } + + if (posInBlock == CMainSignals::SYNC_TRANSACTION_NOT_IN_BLOCK) { + UpdateISLockMinedBlock(islockInfo, nullptr); + return; + } + UpdateISLockMinedBlock(islockInfo, pindex); + } + + if (IsLocked(tx.GetHash())) { + RetryLockMempoolTxs(tx.GetHash()); + } +} + +void CInstantSendManager::NotifyChainLock(const CBlockIndex* pindex) +{ + { + LOCK(cs); + + // Let's find all islocks that correspond to TXs which are part of the freshly ChainLocked chain and then delete + // the islocks. We do this because the ChainLocks imply locking and thus it's not needed to further track + // or propagate the islocks + std::unordered_set toDelete; + while (pindex && pindex != pindexLastChainLock) { + auto its = blockToInstantSendLocks.equal_range(pindex->GetBlockHash()); + while (its.first != its.second) { + auto islockInfo = its.first->second; + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: removing islock as it got ChainLocked in block %s\n", __func__, + islockInfo->islock.txid.ToString(), islockInfo->islockHash.ToString(), pindex->GetBlockHash().ToString()); + toDelete.emplace(its.first->second->islockHash); + ++its.first; + } + + pindex = pindex->pprev; + } + + pindexLastChainLock = pindex; + + for (auto& islockHash : toDelete) { + RemoveFinalISLock(islockHash); + } + } + + RetryLockMempoolTxs(uint256()); +} + +void CInstantSendManager::UpdateISLockMinedBlock(llmq::CInstantSendLockInfo* islockInfo, const CBlockIndex* pindex) +{ + AssertLockHeld(cs); + + if (islockInfo->pindexMined == pindex) { + return; + } + + if (islockInfo->pindexMined) { + auto its = blockToInstantSendLocks.equal_range(islockInfo->pindexMined->GetBlockHash()); + while (its.first != its.second) { + if (its.first->second == islockInfo) { + its.first = blockToInstantSendLocks.erase(its.first); + } else { + ++its.first; + } + } + } + + if (pindex) { + blockToInstantSendLocks.emplace(pindex->GetBlockHash(), islockInfo); + } + + islockInfo->pindexMined = pindex; +} + +void CInstantSendManager::RemoveFinalISLock(const uint256& hash) +{ + AssertLockHeld(cs); + + auto it = finalInstantSendLocks.find(hash); + if (it == finalInstantSendLocks.end()) { + return; + } + auto& islockInfo = it->second; + + txToInstantSendLock.erase(islockInfo.islock.txid); + for (auto& in : islockInfo.islock.inputs) { + auto inputRequestId = ::SerializeHash(std::make_pair(INPUTLOCK_REQUESTID_PREFIX, in)); + inputVotes.erase(inputRequestId); + inputToInstantSendLock.erase(in); + } + UpdateISLockMinedBlock(&islockInfo, nullptr); + finalInstantSendLocks.erase(it); +} + +void CInstantSendManager::RemoveMempoolConflictsForLock(const uint256& hash, const CInstantSendLock& islock) +{ + LOCK(mempool.cs); + + std::unordered_map toDelete; + + for (auto& in : islock.inputs) { + auto it = mempool.mapNextTx.find(in); + if (it == mempool.mapNextTx.end()) { + continue; + } + if (it->second->GetHash() != islock.txid) { + toDelete.emplace(it->second->GetHash(), mempool.get(it->second->GetHash())); + + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s, islock=%s: mempool TX %s with input %s conflicts with islock\n", __func__, + islock.txid.ToString(), hash.ToString(), it->second->GetHash().ToString(), in.ToStringShort()); + } + } + + for (auto& p : toDelete) { + mempool.removeRecursive(*p.second, MemPoolRemovalReason::CONFLICT); + } +} + +void CInstantSendManager::RetryLockMempoolTxs(const uint256& lockedParentTx) +{ + // Let's retry all mempool TXs which don't have an islock yet and where the parents got ChainLocked now + + std::unordered_map txs; + + { + LOCK(mempool.cs); + + if (lockedParentTx.IsNull()) { + txs.reserve(mempool.mapTx.size()); + for (auto it = mempool.mapTx.begin(); it != mempool.mapTx.end(); ++it) { + txs.emplace(it->GetTx().GetHash(), it->GetSharedTx()); + } + } else { + auto it = mempool.mapNextTx.lower_bound(COutPoint(lockedParentTx, 0)); + while (it != mempool.mapNextTx.end() && it->first->hash == lockedParentTx) { + txs.emplace(it->second->GetHash(), mempool.get(it->second->GetHash())); + ++it; + } + } + } + for (auto& p : txs) { + auto& tx = p.second; + { + LOCK(cs); + if (txToCreatingInstantSendLocks.count(tx->GetHash())) { + // we're already in the middle of locking this one + continue; + } + if (IsLocked(tx->GetHash())) { + continue; + } + if (IsConflicted(*tx)) { + // should not really happen as we have already filtered these out + continue; + } + } + + // CheckCanLock is already called by ProcessTx, so we should avoid calling it twice. But we also shouldn't spam + // the logs when retrying TXs that are not ready yet. + if (LogAcceptCategory("instantsend")) { + if (!CheckCanLock(*tx, false, Params().GetConsensus())) { + continue; + } + LogPrint("instantsend", "CInstantSendManager::%s -- txid=%s: retrying to lock\n", __func__, + tx->GetHash().ToString()); + } + + ProcessTx(nullptr, *tx, *g_connman, Params().GetConsensus()); + } +} + +bool CInstantSendManager::AlreadyHave(const CInv& inv) +{ + if (!IsNewInstantSendEnabled()) { + return true; + } + + LOCK(cs); + return finalInstantSendLocks.count(inv.hash) != 0 || pendingInstantSendLocks.count(inv.hash) != 0; +} + +bool CInstantSendManager::GetInstantSendLockByHash(const uint256& hash, llmq::CInstantSendLock& ret) +{ + if (!IsNewInstantSendEnabled()) { + return false; + } + + LOCK(cs); + auto it = finalInstantSendLocks.find(hash); + if (it == finalInstantSendLocks.end()) { + return false; + } + ret = it->second.islock; + return true; +} + +bool CInstantSendManager::IsLocked(const uint256& txHash) +{ + if (!IsNewInstantSendEnabled()) { + return false; + } + + LOCK(cs); + return txToInstantSendLock.count(txHash) != 0; +} + +bool CInstantSendManager::IsConflicted(const CTransaction& tx) +{ + LOCK(cs); + uint256 dummy; + return GetConflictingTx(tx, dummy); +} + +bool CInstantSendManager::GetConflictingTx(const CTransaction& tx, uint256& retConflictTxHash) +{ + if (!IsNewInstantSendEnabled()) { + return false; + } + + LOCK(cs); + for (const auto& in : tx.vin) { + auto it = inputToInstantSendLock.find(in.prevout); + if (it == inputToInstantSendLock.end()) { + continue; + } + + if (it->second->islock.txid != tx.GetHash()) { + retConflictTxHash = it->second->islock.txid; + return true; + } + } + return false; +} + +bool IsOldInstantSendEnabled() +{ + return sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED) && !sporkManager.IsSporkActive(SPORK_20_INSTANTSEND_LLMQ_BASED); +} + +bool IsNewInstantSendEnabled() +{ + return sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED) && sporkManager.IsSporkActive(SPORK_20_INSTANTSEND_LLMQ_BASED); +} + +bool IsInstantSendEnabled() +{ + return sporkManager.IsSporkActive(SPORK_2_INSTANTSEND_ENABLED); +} + +} diff --git a/src/llmq/quorums_instantsend.h b/src/llmq/quorums_instantsend.h new file mode 100644 index 0000000000..3d114970ef --- /dev/null +++ b/src/llmq/quorums_instantsend.h @@ -0,0 +1,147 @@ +// Copyright (c) 2019 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DASH_QUORUMS_INSTANTSEND_H +#define DASH_QUORUMS_INSTANTSEND_H + +#include "quorums_signing.h" + +#include "coins.h" +#include "primitives/transaction.h" + +#include +#include + +class CScheduler; + +namespace llmq +{ + +class CInstantSendLock +{ +public: + std::vector inputs; + uint256 txid; + CBLSSignature sig; + +public: + ADD_SERIALIZE_METHODS + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(inputs); + READWRITE(txid); + READWRITE(sig); + } + + uint256 GetRequestId() const; +}; + +class CInstantSendLockInfo +{ +public: + // might be nullptr when islock is received before the TX itself + CTransactionRef tx; + CInstantSendLock islock; + // only valid when recovered sig was received + uint256 islockHash; + // time when it was created/received + int64_t time; + + // might be null initially (when TX was not mined yet) and will later be filled by SyncTransaction + const CBlockIndex* pindexMined{nullptr}; +}; + +class CInstantSendManager : public CRecoveredSigsListener +{ +private: + CCriticalSection cs; + CScheduler* scheduler; + + /** + * These are the votes/signatures we performed locally. It's indexed by the LLMQ requestId, which is + * hash(TXLOCK_REQUESTID_PREFIX, prevout). The map values are the txids we voted for. This map is used to + * avoid voting for the same input twice. + */ + std::unordered_map inputVotes; + + /** + * These are the islocks that are currently in the middle of being created. Entries are created when we observed + * recovered signatures for all inputs of a TX. At the same time, we initiate signing of our sigshare for the islock. + * When the recovered sig for the islock later arrives, we can finish the islock and propagate it. + */ + std::unordered_map creatingInstantSendLocks; + // maps from txid to the in-progress islock + std::unordered_map txToCreatingInstantSendLocks; + + /** + * These are the final islocks, indexed by their own hash. The other maps are used to get from TXs, inputs and blocks + * to islocks. + */ + std::unordered_map finalInstantSendLocks; + std::unordered_map txToInstantSendLock; + std::unordered_map inputToInstantSendLock; + std::unordered_multimap blockToInstantSendLocks; + + const CBlockIndex* pindexLastChainLock{nullptr}; + + // Incoming and not verified yet + std::unordered_map> pendingInstantSendLocks; + bool hasScheduledProcessPending{false}; + +public: + CInstantSendManager(CScheduler* _scheduler); + ~CInstantSendManager(); + + void RegisterAsRecoveredSigsListener(); + void UnregisterAsRecoveredSigsListener(); + +public: + bool ProcessTx(CNode* pfrom, const CTransaction& tx, CConnman& connman, const Consensus::Params& params); + bool CheckCanLock(const CTransaction& tx, bool printDebug, const Consensus::Params& params); + bool CheckCanLock(const COutPoint& outpoint, bool printDebug, const uint256* _txHash, CAmount* retValue, const Consensus::Params& params); + bool IsLocked(const uint256& txHash); + bool IsConflicted(const CTransaction& tx); + bool GetConflictingTx(const CTransaction& tx, uint256& retConflictTxHash); + + virtual void HandleNewRecoveredSig(const CRecoveredSig& recoveredSig); + void HandleNewInputLockRecoveredSig(const CRecoveredSig& recoveredSig, const uint256& txid); + void HandleNewInstantSendLockRecoveredSig(const CRecoveredSig& recoveredSig); + + void TrySignInstantSendLock(const CTransaction& tx); + + void ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, CConnman& connman); + void ProcessMessageInstantSendLock(CNode* pfrom, const CInstantSendLock& islock, CConnman& connman); + bool PreVerifyInstantSendLock(NodeId nodeId, const CInstantSendLock& islock, bool& retBan); + void ProcessPendingInstantSendLocks(); + void ProcessInstantSendLock(NodeId from, const uint256& hash, const CInstantSendLock& islock); + void UpdateWalletTransaction(const uint256& txid); + + void SyncTransaction(const CTransaction &tx, const CBlockIndex *pindex, int posInBlock); + void NotifyChainLock(const CBlockIndex* pindex); + void UpdateISLockMinedBlock(CInstantSendLockInfo* islockInfo, const CBlockIndex* pindex); + void RemoveFinalISLock(const uint256& hash); + + void RemoveMempoolConflictsForLock(const uint256& hash, const CInstantSendLock& islock); + void RetryLockMempoolTxs(const uint256& lockedParentTx); + + bool AlreadyHave(const CInv& inv); + bool GetInstantSendLockByHash(const uint256& hash, CInstantSendLock& ret); +}; + +extern CInstantSendManager* quorumInstantSendManager; + +// This involves 2 sporks: SPORK_2_INSTANTSEND_ENABLED and SPORK_20_INSTANTSEND_LLMQ_BASED +// SPORK_2_INSTANTSEND_ENABLED generally enables/disables InstantSend and SPORK_20_INSTANTSEND_LLMQ_BASED switches +// between the old and the new (LLMQ based) system +// TODO When the new system is fully deployed and enabled, we can remove this special handling in a future version +// and revert to only using SPORK_2_INSTANTSEND_ENABLED. +bool IsOldInstantSendEnabled(); +bool IsNewInstantSendEnabled(); +bool IsInstantSendEnabled(); + +} + +#endif//DASH_QUORUMS_INSTANTSEND_H diff --git a/src/llmq/quorums_signing.cpp b/src/llmq/quorums_signing.cpp index 2239d364da..7c2f4e0ce5 100644 --- a/src/llmq/quorums_signing.cpp +++ b/src/llmq/quorums_signing.cpp @@ -440,11 +440,25 @@ void CSigningManager::CollectPendingRecoveredSigsToVerify( } } +void CSigningManager::ProcessPendingReconstructedRecoveredSigs() +{ + decltype(pendingReconstructedRecoveredSigs) l; + { + LOCK(cs); + l = std::move(pendingReconstructedRecoveredSigs); + } + for (auto& p : l) { + ProcessRecoveredSig(-1, p.first, p.second, *g_connman); + } +} + bool CSigningManager::ProcessPendingRecoveredSigs(CConnman& connman) { std::unordered_map> recSigsByNode; std::unordered_map, CQuorumCPtr, StaticSaltedHasher> quorums; + ProcessPendingReconstructedRecoveredSigs(); + CollectPendingRecoveredSigsToVerify(32, recSigsByNode, quorums); if (recSigsByNode.empty()) { return false; @@ -529,7 +543,7 @@ void CSigningManager::ProcessRecoveredSig(NodeId nodeId, const CRecoveredSig& re } else { // Looks like we're trying to process a recSig that is already known. This might happen if the same // recSig comes in through regular QRECSIG messages and at the same time through some other message - // which allowed to reconstruct a recSig (e.g. IXLOCK). In this case, just bail out. + // which allowed to reconstruct a recSig (e.g. ISLOCK). In this case, just bail out. } return; } else { @@ -550,6 +564,12 @@ void CSigningManager::ProcessRecoveredSig(NodeId nodeId, const CRecoveredSig& re } } +void CSigningManager::PushReconstructedRecoveredSig(const llmq::CRecoveredSig& recoveredSig, const llmq::CQuorumCPtr& quorum) +{ + LOCK(cs); + pendingReconstructedRecoveredSigs.emplace_back(recoveredSig, quorum); +} + void CSigningManager::Cleanup() { int64_t now = GetTimeMillis(); diff --git a/src/llmq/quorums_signing.h b/src/llmq/quorums_signing.h index 1d00243515..3c379e4bf3 100644 --- a/src/llmq/quorums_signing.h +++ b/src/llmq/quorums_signing.h @@ -126,6 +126,7 @@ private: // Incoming and not verified yet std::unordered_map> pendingRecoveredSigs; + std::list> pendingReconstructedRecoveredSigs; // must be protected by cs FastRandomContext rnd; @@ -142,6 +143,10 @@ public: void ProcessMessage(CNode* pnode, const std::string& strCommand, CDataStream& vRecv, CConnman& connman); + // This is called when a recovered signature was was reconstructed from another P2P message and is known to be valid + // This is the case for example when a signature appears as part of InstantSend or ChainLocks + void PushReconstructedRecoveredSig(const CRecoveredSig& recoveredSig, const CQuorumCPtr& quorum); + private: void ProcessMessageRecoveredSig(CNode* pfrom, const CRecoveredSig& recoveredSig, CConnman& connman); bool PreVerifyRecoveredSig(NodeId nodeId, const CRecoveredSig& recoveredSig, bool& retBan); @@ -149,6 +154,7 @@ private: void CollectPendingRecoveredSigsToVerify(size_t maxUniqueSessions, std::unordered_map>& retSigShares, std::unordered_map, CQuorumCPtr, StaticSaltedHasher>& retQuorums); + void ProcessPendingReconstructedRecoveredSigs(); bool ProcessPendingRecoveredSigs(CConnman& connman); // called from the worker thread of CSigSharesManager void ProcessRecoveredSig(NodeId nodeId, const CRecoveredSig& recoveredSig, const CQuorumCPtr& quorum, CConnman& connman); void Cleanup(); // called from the worker thread of CSigSharesManager diff --git a/src/miner.cpp b/src/miner.cpp index c07061f329..c4c8490693 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -34,6 +34,7 @@ #include "evo/deterministicmns.h" #include "llmq/quorums_blockprocessor.h" +#include "llmq/quorums_chainlocks.h" #include #include @@ -286,11 +287,15 @@ bool BlockAssembler::TestPackage(uint64_t packageSize, unsigned int packageSigOp // Perform transaction-level checks before adding to block: // - transaction finality (locktime) +// - safe TXs in regard to ChainLocks bool BlockAssembler::TestPackageTransactions(const CTxMemPool::setEntries& package) { BOOST_FOREACH (const CTxMemPool::txiter it, package) { if (!IsFinalTx(it->GetTx(), nHeight, nLockTimeCutoff)) return false; + if (!llmq::chainLocksHandler->IsTxSafeForMining(it->GetTx().GetHash())) { + return false; + } } return true; } @@ -332,6 +337,10 @@ bool BlockAssembler::TestForBlock(CTxMemPool::txiter iter) if (!IsFinalTx(iter->GetTx(), nHeight, nLockTimeCutoff)) return false; + if (!llmq::chainLocksHandler->IsTxSafeForMining(iter->GetTx().GetHash())) { + return false; + } + return true; } @@ -523,7 +532,7 @@ void BlockAssembler::addPackageTxs(int &nPackagesSelected, int &nDescendantsUpda onlyUnconfirmed(ancestors); ancestors.insert(iter); - // Test if all tx's are Final + // Test if all tx's are Final and safe if (!TestPackageTransactions(ancestors)) { if (fUsingModified) { mapModifiedTx.get().erase(modit); diff --git a/src/net.cpp b/src/net.cpp index fd20a51ee0..b54147c02d 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -28,6 +28,7 @@ #include "instantx.h" #include "masternode-sync.h" #include "privatesend.h" +#include "llmq/quorums_instantsend.h" #ifdef WIN32 #include @@ -2786,8 +2787,12 @@ bool CConnman::DisconnectNode(NodeId id) void CConnman::RelayTransaction(const CTransaction& tx) { uint256 hash = tx.GetHash(); - int nInv = static_cast(CPrivateSend::GetDSTX(hash)) ? MSG_DSTX : - (instantsend.HasTxLockRequest(hash) ? MSG_TXLOCK_REQUEST : MSG_TX); + int nInv = MSG_TX; + if (CPrivateSend::GetDSTX(hash)) { + nInv = MSG_DSTX; + } else if (llmq::IsOldInstantSendEnabled() && instantsend.HasTxLockRequest(hash)) { + nInv = MSG_TXLOCK_REQUEST; + } CInv inv(nInv, hash); LOCK(cs_vNodes); BOOST_FOREACH(CNode* pnode, vNodes) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index b68c0bcf03..8df5011411 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -50,6 +50,7 @@ #include "llmq/quorums_debug.h" #include "llmq/quorums_dkgsessionmgr.h" #include "llmq/quorums_init.h" +#include "llmq/quorums_instantsend.h" #include "llmq/quorums_signing.h" #include "llmq/quorums_signing_shares.h" @@ -976,6 +977,8 @@ bool static AlreadyHave(const CInv& inv) EXCLUSIVE_LOCKS_REQUIRED(cs_main) return llmq::quorumSigningManager->AlreadyHave(inv); case MSG_CLSIG: return llmq::chainLocksHandler->AlreadyHave(inv); + case MSG_ISLOCK: + return llmq::quorumInstantSendManager->AlreadyHave(inv); } // Don't know what it is, just say we already got one @@ -1296,6 +1299,14 @@ void static ProcessGetData(CNode* pfrom, const Consensus::Params& consensusParam } } + if (!push && (inv.type == MSG_ISLOCK)) { + llmq::CInstantSendLock o; + if (llmq::quorumInstantSendManager->GetInstantSendLockByHash(inv.hash, o)) { + connman.PushMessage(pfrom, msgMaker.Make(NetMsgType::ISLOCK, o)); + push = true; + } + } + if (!push) vNotFound.push_back(inv); } @@ -1777,6 +1788,9 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr case MSG_CLSIG: doubleRequestDelay = 5 * 1000000; break; + case MSG_ISLOCK: + doubleRequestDelay = 5 * 1000000; + break; } pfrom->AskFor(inv, doubleRequestDelay); } @@ -2001,11 +2015,16 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr if(strCommand == NetMsgType::TX) { vRecv >> ptx; txLockRequest = CTxLockRequest(ptx); - fCanAutoLock = CInstantSend::CanAutoLock() && txLockRequest.IsSimple(); + fCanAutoLock = llmq::IsOldInstantSendEnabled() && CInstantSend::CanAutoLock() && txLockRequest.IsSimple(); } else if(strCommand == NetMsgType::TXLOCKREQUEST) { vRecv >> txLockRequest; ptx = txLockRequest.tx; nInvType = MSG_TXLOCK_REQUEST; + if (llmq::IsNewInstantSendEnabled()) { + // the new system does not require explicit lock requests + // changing the inv type to MSG_TX also results in re-broadcasting the TX as normal TX + nInvType = MSG_TX; + } } else if (strCommand == NetMsgType::DSTX) { vRecv >> dstx; ptx = dstx.tx; @@ -2021,7 +2040,7 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr } // Process custom logic, no matter if tx will be accepted to mempool later or not - if (strCommand == NetMsgType::TXLOCKREQUEST || fCanAutoLock) { + if (nInvType == MSG_TXLOCK_REQUEST || fCanAutoLock) { if(!instantsend.ProcessTxLockRequest(txLockRequest, connman)) { LogPrint("instantsend", "TXLOCKREQUEST -- failed %s\n", txLockRequest.GetHash().ToString()); // Should not really happen for "fCanAutoLock == true" but just in case: @@ -2032,7 +2051,7 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr // Fallback for normal txes to process as usual fCanAutoLock = false; } - } else if (strCommand == NetMsgType::DSTX) { + } else if (nInvType == MSG_DSTX) { uint256 hashTx = tx.GetHash(); if(CPrivateSend::GetDSTX(hashTx)) { LogPrint("privatesend", "DSTX -- Already have %s, skipping...\n", hashTx.ToString()); @@ -2069,17 +2088,21 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr if (!AlreadyHave(inv) && AcceptToMemoryPool(mempool, state, ptx, true, &fMissingInputs)) { // Process custom txes, this changes AlreadyHave to "true" - if (strCommand == NetMsgType::DSTX) { + if (nInvType == MSG_DSTX) { LogPrintf("DSTX -- Masternode transaction accepted, txid=%s, peer=%d\n", tx.GetHash().ToString(), pfrom->id); CPrivateSend::AddDSTX(dstx); - } else if (strCommand == NetMsgType::TXLOCKREQUEST || fCanAutoLock) { + } else if (nInvType == MSG_TXLOCK_REQUEST || fCanAutoLock) { LogPrintf("TXLOCKREQUEST -- Transaction Lock Request accepted, txid=%s, peer=%d\n", tx.GetHash().ToString(), pfrom->id); instantsend.AcceptLockRequest(txLockRequest); instantsend.Vote(tx.GetHash(), connman); } + if (nInvType != MSG_TXLOCK_REQUEST) { + llmq::quorumInstantSendManager->ProcessTx(pfrom, tx, connman, chainparams.GetConsensus()); + } + mempool.check(pcoinsTip); connman.RelayTransaction(tx); for (unsigned int i = 0; i < tx.vout.size(); i++) { @@ -2124,6 +2147,8 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr vWorkQueue.emplace_back(orphanHash, i); } vEraseQueue.push_back(orphanHash); + + llmq::quorumInstantSendManager->ProcessTx(pfrom, orphanTx, connman, chainparams.GetConsensus()); } else if (!fMissingInputs2) { @@ -2188,7 +2213,7 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr } } - if (strCommand == NetMsgType::TXLOCKREQUEST && !AlreadyHave(inv)) { + if (nInvType == MSG_TXLOCK_REQUEST && !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. @@ -2943,6 +2968,7 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr llmq::quorumSigSharesManager->ProcessMessage(pfrom, strCommand, vRecv, connman); llmq::quorumSigningManager->ProcessMessage(pfrom, strCommand, vRecv, connman); llmq::chainLocksHandler->ProcessMessage(pfrom, strCommand, vRecv, connman); + llmq::quorumInstantSendManager->ProcessMessage(pfrom, strCommand, vRecv, connman); } else { diff --git a/src/protocol.cpp b/src/protocol.cpp index 6df80cae74..48407d640e 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -71,6 +71,7 @@ const char *QGETSIGSHARES="qgetsigs"; const char *QBSIGSHARES="qbsigs"; const char *QSIGREC="qsigrec"; const char *CLSIG="clsig"; +const char *ISLOCK="islock"; }; static const char* ppszTypeName[] = @@ -107,6 +108,7 @@ static const char* ppszTypeName[] = NetMsgType::QDEBUGSTATUS, NetMsgType::QSIGREC, NetMsgType::CLSIG, + NetMsgType::ISLOCK, }; /** All known message types. Keep this in the same order as the list of @@ -172,6 +174,7 @@ const static std::string allNetMessageTypes[] = { NetMsgType::QBSIGSHARES, NetMsgType::QSIGREC, NetMsgType::CLSIG, + NetMsgType::ISLOCK, }; const static std::vector allNetMessageTypesVec(allNetMessageTypes, allNetMessageTypes+ARRAYLEN(allNetMessageTypes)); diff --git a/src/protocol.h b/src/protocol.h index b4d6eab3ec..c2027b3e33 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -277,6 +277,7 @@ extern const char *QGETSIGSHARES; extern const char *QBSIGSHARES; extern const char *QSIGREC; extern const char *CLSIG; +extern const char *ISLOCK; }; /* Get a vector of all valid message types (see above) */ @@ -379,6 +380,7 @@ enum GetDataMsg { MSG_QUORUM_DEBUG_STATUS = 27, MSG_QUORUM_RECOVERED_SIG = 28, MSG_CLSIG = 29, + MSG_ISLOCK = 30, }; /** inv message data */ diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp index f6069f24cf..7f3385dfd8 100644 --- a/src/qt/transactiondesc.cpp +++ b/src/qt/transactiondesc.cpp @@ -21,6 +21,8 @@ #include "instantx.h" +#include "llmq/quorums_instantsend.h" + #include #include @@ -52,6 +54,11 @@ QString TransactionDesc::FormatTxStatus(const CWalletTx& wtx) strTxStatus = tr("%1 confirmations").arg(nDepth); } + if (llmq::quorumInstantSendManager->IsLocked(wtx.GetHash())) { + strTxStatus += tr(" (verified via LLMQ based InstantSend)"); + return strTxStatus; + } + if(!instantsend.HasTxLockRequest(wtx.GetHash())) return strTxStatus; // regular tx int nSignatures = instantsend.GetTransactionLockSignatures(wtx.GetHash()); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 15f13a25b2..487bcca3ca 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -26,6 +26,7 @@ #include "instantx.h" #include "spork.h" #include "privatesend-client.h" +#include "llmq/quorums_instantsend.h" #include @@ -389,7 +390,12 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction &tran CReserveKey *keyChange = transaction.getPossibleKeyChange(); CValidationState state; - if(!wallet->CommitTransaction(*newTx, *keyChange, g_connman.get(), state, recipients[0].fUseInstantSend ? NetMsgType::TXLOCKREQUEST : NetMsgType::TX)) + // the new IX system does not require explicit IX messages + std::string strCommand = NetMsgType::TX; + if (recipients[0].fUseInstantSend && llmq::IsOldInstantSendEnabled()) { + strCommand = NetMsgType::TXLOCKREQUEST; + } + if(!wallet->CommitTransaction(*newTx, *keyChange, g_connman.get(), state, strCommand)) return SendCoinsReturn(TransactionCommitFailed, QString::fromStdString(state.GetRejectReason())); CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 7aaf3bcd2b..de5bd13ceb 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -28,6 +28,7 @@ #include "evo/cbtx.h" #include "llmq/quorums_chainlocks.h" +#include "llmq/quorums_instantsend.h" #include @@ -409,7 +410,7 @@ void entryToJSON(UniValue &info, const CTxMemPoolEntry &e) info.push_back(Pair("depends", depends)); info.push_back(Pair("instantsend", instantsend.HasTxLockRequest(tx.GetHash()))); - info.push_back(Pair("instantlock", instantsend.IsLockedInstantSendTransaction(tx.GetHash()))); + info.push_back(Pair("instantlock", instantsend.IsLockedInstantSendTransaction(tx.GetHash()) || llmq::quorumInstantSendManager->IsLocked(tx.GetHash()))); } UniValue mempoolToJSON(bool fVerbose = false) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 4a8bdb078d..afdece3aab 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -36,6 +36,7 @@ #include "llmq/quorums_chainlocks.h" #include "llmq/quorums_commitment.h" +#include "llmq/quorums_instantsend.h" #include @@ -199,7 +200,8 @@ void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry) } } bool fLocked = instantsend.IsLockedInstantSendTransaction(txid); - entry.push_back(Pair("instantlock", fLocked)); + bool fLLMQLocked = llmq::quorumInstantSendManager->IsLocked(txid); + entry.push_back(Pair("instantlock", fLocked || fLLMQLocked)); entry.push_back(Pair("chainlock", chainLock)); } @@ -1018,6 +1020,7 @@ UniValue sendrawtransaction(const JSONRPCRequest& request) throw JSONRPCError(RPC_TRANSACTION_ERROR, state.GetRejectReason()); } } + llmq::quorumInstantSendManager->ProcessTx(nullptr, *tx, *g_connman, Params().GetConsensus()); } else if (fHaveChain) { throw JSONRPCError(RPC_TRANSACTION_ALREADY_IN_CHAIN, "transaction already in block chain"); } diff --git a/src/spork.cpp b/src/spork.cpp index fe4fbf2967..b12d04bcfe 100644 --- a/src/spork.cpp +++ b/src/spork.cpp @@ -29,6 +29,7 @@ std::map mapSporkDefaults = { {SPORK_17_QUORUM_DKG_ENABLED, 4070908800ULL}, // OFF {SPORK_18_QUORUM_DEBUG_ENABLED, 4070908800ULL}, // OFF {SPORK_19_CHAINLOCKS_ENABLED, 4070908800ULL}, // OFF + {SPORK_20_INSTANTSEND_LLMQ_BASED, 4070908800ULL}, // OFF }; bool CSporkManager::SporkValueIsActive(int nSporkID, int64_t &nActiveValueRet) const @@ -291,6 +292,7 @@ int CSporkManager::GetSporkIDByName(const std::string& strName) if (strName == "SPORK_17_QUORUM_DKG_ENABLED") return SPORK_17_QUORUM_DKG_ENABLED; if (strName == "SPORK_18_QUORUM_DEBUG_ENABLED") return SPORK_18_QUORUM_DEBUG_ENABLED; if (strName == "SPORK_19_CHAINLOCKS_ENABLED") return SPORK_19_CHAINLOCKS_ENABLED; + if (strName == "SPORK_20_INSTANTSEND_LLMQ_BASED") return SPORK_20_INSTANTSEND_LLMQ_BASED; LogPrint("spork", "CSporkManager::GetSporkIDByName -- Unknown Spork name '%s'\n", strName); return -1; @@ -310,6 +312,7 @@ std::string CSporkManager::GetSporkNameByID(int nSporkID) case SPORK_17_QUORUM_DKG_ENABLED: return "SPORK_17_QUORUM_DKG_ENABLED"; case SPORK_18_QUORUM_DEBUG_ENABLED: return "SPORK_18_QUORUM_DEBUG_ENABLED"; case SPORK_19_CHAINLOCKS_ENABLED: return "SPORK_19_CHAINLOCKS_ENABLED"; + case SPORK_20_INSTANTSEND_LLMQ_BASED: return "SPORK_20_INSTANTSEND_LLMQ_BASED"; default: LogPrint("spork", "CSporkManager::GetSporkNameByID -- Unknown Spork ID %d\n", nSporkID); return "Unknown"; diff --git a/src/spork.h b/src/spork.h index 55d9d62305..e02ac30623 100644 --- a/src/spork.h +++ b/src/spork.h @@ -28,9 +28,10 @@ static const int SPORK_16_INSTANTSEND_AUTOLOCKS = 10015; static const int SPORK_17_QUORUM_DKG_ENABLED = 10016; static const int SPORK_18_QUORUM_DEBUG_ENABLED = 10017; static const int SPORK_19_CHAINLOCKS_ENABLED = 10018; +static const int SPORK_20_INSTANTSEND_LLMQ_BASED = 10019; static const int SPORK_START = SPORK_2_INSTANTSEND_ENABLED; -static const int SPORK_END = SPORK_19_CHAINLOCKS_ENABLED; +static const int SPORK_END = SPORK_20_INSTANTSEND_LLMQ_BASED; extern std::map mapSporkDefaults; extern CSporkManager sporkManager; diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 295f31cbba..9575ba99ca 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -24,6 +24,8 @@ #include "evo/specialtx.h" #include "evo/providertx.h" +#include "llmq/quorums_instantsend.h" + CTxMemPoolEntry::CTxMemPoolEntry(const CTransactionRef& _tx, const CAmount& _nFee, int64_t _nTime, double _entryPriority, unsigned int _entryHeight, CAmount _inChainInputValue, @@ -1497,7 +1499,7 @@ int CTxMemPool::Expire(int64_t time) { setEntries toremove; while (it != mapTx.get().end() && it->GetTime() < time) { // locked txes do not expire until mined and have sufficient confirmations - if (instantsend.IsLockedInstantSendTransaction(it->GetTx().GetHash())) { + if (instantsend.IsLockedInstantSendTransaction(it->GetTx().GetHash()) || llmq::quorumInstantSendManager->IsLocked(it->GetTx().GetHash())) { it++; continue; } diff --git a/src/validation.cpp b/src/validation.cpp index e57409bcf6..ea042a42dd 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -46,6 +46,7 @@ #include "evo/deterministicmns.h" #include "evo/cbtx.h" +#include "llmq/quorums_instantsend.h" #include "llmq/quorums_chainlocks.h" #include @@ -692,6 +693,18 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState& state, const C REJECT_INVALID, "tx-txlock-conflict"); } + uint256 txConflictHash; + if (llmq::quorumInstantSendManager->GetConflictingTx(tx, txConflictHash)) { + CTransactionRef txConflict; + uint256 hashBlock; + if (GetTransaction(txConflictHash, txConflict, Params().GetConsensus(), hashBlock)) { + GetMainSignals().NotifyInstantSendDoubleSpendAttempt(tx, *txConflict); + } + return state.DoS(10, error("AcceptToMemoryPool : Transaction %s conflicts with locked TX %s", + hash.ToString(), txConflictHash.ToString()), + REJECT_INVALID, "tx-txlock-conflict"); + } + // Check for conflicts with in-memory transactions { LOCK(pool.cs); // protect pool.mapNextTx @@ -2196,13 +2209,49 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd LogPrint("bench", " - Verify %u txins: %.2fms (%.3fms/txin) [%.2fs]\n", nInputs - 1, 0.001 * (nTime4 - nTime2), nInputs <= 1 ? 0 : 0.001 * (nTime4 - nTime2) / (nInputs-1), nTimeVerify * 0.000001); - // DASH : MODIFIED TO CHECK MASTERNODE PAYMENTS AND SUPERBLOCKS + // DASH // It's possible that we simply don't have enough data and this could fail // (i.e. block itself could be a correct one and we need to store it), // that's why this is in ConnectBlock. Could be the other way around however - // the peer who sent us this block is missing some data and wasn't able // to recognize that block is actually invalid. + + // DASH : CHECK TRANSACTIONS FOR INSTANTSEND + + if (sporkManager.IsSporkActive(SPORK_3_INSTANTSEND_BLOCK_FILTERING)) { + // Require other nodes to comply, send them some data in case they are missing it. + for (const auto& tx : block.vtx) { + // skip txes that have no inputs + if (tx->vin.empty()) continue; + // LOOK FOR TRANSACTION LOCK IN OUR MAP OF OUTPOINTS + for (const auto& txin : tx->vin) { + uint256 hashLocked; + if (instantsend.GetLockedOutPointTxHash(txin.prevout, hashLocked) && hashLocked != tx->GetHash()) { + // The node which relayed this should switch to correct chain. + // TODO: relay instantsend data/proof. + LOCK(cs_main); + mapRejectedBlocks.insert(std::make_pair(block.GetHash(), GetTime())); + return state.DoS(10, error("ConnectBlock(DASH): transaction %s conflicts with transaction lock %s", tx->GetHash().ToString(), hashLocked.ToString()), + REJECT_INVALID, "conflict-tx-lock"); + } + } + uint256 txConflict; + if (llmq::quorumInstantSendManager->GetConflictingTx(*tx, txConflict)) { + // The node which relayed this should switch to correct chain. + // TODO: relay instantsend data/proof. + LOCK(cs_main); + mapRejectedBlocks.insert(std::make_pair(block.GetHash(), GetTime())); + return state.DoS(10, error("ConnectBlock(DASH): transaction %s conflicts with transaction lock %s", tx->GetHash().ToString(), txConflict.ToString()), + REJECT_INVALID, "conflict-tx-lock"); + } + } + } else { + LogPrintf("ConnectBlock(DASH): spork is off, skipping transaction locking checks\n"); + } + + // DASH : MODIFIED TO CHECK MASTERNODE PAYMENTS AND SUPERBLOCKS + // TODO: resync data (both ways?) and try to reprocess this block later. CAmount blockReward = nFees + GetBlockSubsidy(pindex->pprev->nBits, pindex->pprev->nHeight, chainparams.GetConsensus()); std::string strError = ""; @@ -3256,35 +3305,6 @@ bool CheckBlock(const CBlock& block, CValidationState& state, const Consensus::P if (block.vtx[i]->IsCoinBase()) return state.DoS(100, false, REJECT_INVALID, "bad-cb-multiple", false, "more than one coinbase"); - - // 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. - for(const auto& tx : block.vtx) { - // skip coinbase, it has no inputs - if (tx->IsCoinBase()) continue; - // LOOK FOR TRANSACTION LOCK IN OUR MAP OF OUTPOINTS - for (const auto& txin : tx->vin) { - uint256 hashLocked; - if(instantsend.GetLockedOutPointTxHash(txin.prevout, hashLocked) && hashLocked != tx->GetHash()) { - // The node which relayed this will have to switch later, - // relaying instantsend data won't help it. - LOCK(cs_main); - mapRejectedBlocks.insert(std::make_pair(block.GetHash(), GetTime())); - return state.DoS(100, false, REJECT_INVALID, "conflict-tx-lock", false, - strprintf("transaction %s conflicts with transaction lock %s", tx->GetHash().ToString(), hashLocked.ToString())); - } - } - } - } else { - LogPrintf("CheckBlock(DASH): spork is off, skipping transaction locking checks\n"); - } - - // END DASH - // Check transactions for (const auto& tx : block.vtx) if (!CheckTransaction(*tx, state)) diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp index 86f374ec16..dc928f1b42 100644 --- a/src/validationinterface.cpp +++ b/src/validationinterface.cpp @@ -18,6 +18,7 @@ void RegisterValidationInterface(CValidationInterface* pwalletIn) { g_signals.UpdatedBlockTip.connect(boost::bind(&CValidationInterface::UpdatedBlockTip, pwalletIn, _1, _2, _3)); g_signals.SyncTransaction.connect(boost::bind(&CValidationInterface::SyncTransaction, pwalletIn, _1, _2, _3)); g_signals.NotifyTransactionLock.connect(boost::bind(&CValidationInterface::NotifyTransactionLock, pwalletIn, _1)); + g_signals.NotifyChainLock.connect(boost::bind(&CValidationInterface::NotifyChainLock, pwalletIn, _1)); g_signals.UpdatedTransaction.connect(boost::bind(&CValidationInterface::UpdatedTransaction, pwalletIn, _1)); g_signals.SetBestChain.connect(boost::bind(&CValidationInterface::SetBestChain, pwalletIn, _1)); g_signals.Inventory.connect(boost::bind(&CValidationInterface::Inventory, pwalletIn, _1)); @@ -40,6 +41,7 @@ void UnregisterValidationInterface(CValidationInterface* pwalletIn) { g_signals.Inventory.disconnect(boost::bind(&CValidationInterface::Inventory, pwalletIn, _1)); g_signals.SetBestChain.disconnect(boost::bind(&CValidationInterface::SetBestChain, pwalletIn, _1)); g_signals.UpdatedTransaction.disconnect(boost::bind(&CValidationInterface::UpdatedTransaction, pwalletIn, _1)); + g_signals.NotifyChainLock.disconnect(boost::bind(&CValidationInterface::NotifyChainLock, pwalletIn, _1)); g_signals.NotifyTransactionLock.disconnect(boost::bind(&CValidationInterface::NotifyTransactionLock, pwalletIn, _1)); g_signals.SyncTransaction.disconnect(boost::bind(&CValidationInterface::SyncTransaction, pwalletIn, _1, _2, _3)); g_signals.UpdatedBlockTip.disconnect(boost::bind(&CValidationInterface::UpdatedBlockTip, pwalletIn, _1, _2, _3)); @@ -61,6 +63,7 @@ void UnregisterAllValidationInterfaces() { g_signals.SetBestChain.disconnect_all_slots(); g_signals.UpdatedTransaction.disconnect_all_slots(); g_signals.NotifyTransactionLock.disconnect_all_slots(); + g_signals.NotifyChainLock.disconnect_all_slots(); g_signals.SyncTransaction.disconnect_all_slots(); g_signals.UpdatedBlockTip.disconnect_all_slots(); g_signals.NewPoWValidBlock.disconnect_all_slots(); diff --git a/src/validationinterface.h b/src/validationinterface.h index f28144ed97..5ad8e6ac77 100644 --- a/src/validationinterface.h +++ b/src/validationinterface.h @@ -39,6 +39,7 @@ protected: virtual void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) {} virtual void SyncTransaction(const CTransaction &tx, const CBlockIndex *pindex, int posInBlock) {} virtual void NotifyTransactionLock(const CTransaction &tx) {} + virtual void NotifyChainLock(const CBlockIndex* pindex) {} virtual void NotifyGovernanceVote(const CGovernanceVote &vote) {} virtual void NotifyGovernanceObject(const CGovernanceObject &object) {} virtual void NotifyInstantSendDoubleSpendAttempt(const CTransaction ¤tTx, const CTransaction &previousTx) {} @@ -76,6 +77,8 @@ struct CMainSignals { boost::signals2::signal SyncTransaction; /** Notifies listeners of an updated transaction lock without new data. */ boost::signals2::signal NotifyTransactionLock; + /** Notifies listeners of a ChainLock. */ + boost::signals2::signal NotifyChainLock; /** Notifies listeners of a new governance vote. */ boost::signals2::signal NotifyGovernanceVote; /** Notifies listeners of a new governance object. */ diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 530f7d7257..9074d78c13 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -23,6 +23,7 @@ #include "privatesend-client.h" #include "llmq/quorums_chainlocks.h" +#include "llmq/quorums_instantsend.h" #include @@ -65,12 +66,13 @@ void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry) AssertLockHeld(cs_main); // for mapBlockIndex int confirms = wtx.GetDepthInMainChain(); bool fLocked = instantsend.IsLockedInstantSendTransaction(wtx.GetHash()); + bool fLLMQLocked = llmq::quorumInstantSendManager->IsLocked(wtx.GetHash()); bool chainlock = false; if (confirms > 0) { chainlock = llmq::chainLocksHandler->HasChainLock(mapBlockIndex[wtx.hashBlock]->nHeight, wtx.hashBlock); } entry.push_back(Pair("confirmations", confirms)); - entry.push_back(Pair("instantlock", fLocked)); + entry.push_back(Pair("instantlock", fLocked || fLLMQLocked)); entry.push_back(Pair("chainlock", chainlock)); if (wtx.IsCoinBase()) entry.push_back(Pair("generated", true)); @@ -384,7 +386,12 @@ static void SendMoney(CWallet * const pwallet, const CTxDestination &address, CA throw JSONRPCError(RPC_WALLET_ERROR, strError); } CValidationState state; - if (!pwallet->CommitTransaction(wtxNew, reservekey, g_connman.get(), state, fUseInstantSend ? NetMsgType::TXLOCKREQUEST : NetMsgType::TX)) { + // the new IX system does not require explicit IX messages + std::string strCommand = NetMsgType::TX; + if (fUseInstantSend && llmq::IsOldInstantSendEnabled()) { + strCommand = NetMsgType::TXLOCKREQUEST; + } + if (!pwallet->CommitTransaction(wtxNew, reservekey, g_connman.get(), state, strCommand)) { strError = strprintf("Error: The transaction was rejected! Reason given: %s", state.GetRejectReason()); throw JSONRPCError(RPC_WALLET_ERROR, strError); } @@ -1134,7 +1141,12 @@ UniValue sendmany(const JSONRPCRequest& request) if (!fCreated) throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strFailReason); CValidationState state; - if (!pwallet->CommitTransaction(wtx, keyChange, g_connman.get(), state, fUseInstantSend ? NetMsgType::TXLOCKREQUEST : NetMsgType::TX)) { + // the new IX system does not require explicit IX messages + std::string strCommand = NetMsgType::TX; + if (fUseInstantSend && llmq::IsOldInstantSendEnabled()) { + strCommand = NetMsgType::TXLOCKREQUEST; + } + if (!pwallet->CommitTransaction(wtx, keyChange, g_connman.get(), state, strCommand)) { strFailReason = strprintf("Transaction commit failed:: %s", state.GetRejectReason()); throw JSONRPCError(RPC_WALLET_ERROR, strFailReason); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 8c34403b35..b91a8f7071 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -36,6 +36,8 @@ #include "evo/providertx.h" +#include "llmq/quorums_instantsend.h" + #include #include @@ -1919,6 +1921,9 @@ bool CWalletTx::RelayWalletTransaction(CConnman* connman, const std::string& str instantsend.RejectLockRequest((CTxLockRequest)*this); } } + + llmq::quorumInstantSendManager->ProcessTx(nullptr, *this->tx, *connman, Params().GetConsensus()); + if (connman) { connman->RelayTransaction((CTransaction)*this); return true; @@ -3340,6 +3345,12 @@ bool CWallet::ConvertList(std::vector vecTxIn, std::vector& vecA bool CWallet::CreateTransaction(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosInOut, std::string& strFailReason, const CCoinControl* coinControl, bool sign, AvailableCoinsType nCoinType, bool fUseInstantSend, int nExtraPayloadSize) { + if (!llmq::IsOldInstantSendEnabled()) { + // The new system does not require special handling for InstantSend as this is all done in CInstantSendManager. + // There is also no need for an extra fee anymore. + fUseInstantSend = false; + } + CAmount nFeePay = fUseInstantSend ? CTxLockRequest().GetMinFee(true) : 0; CAmount nValue = 0; @@ -5426,7 +5437,7 @@ int CMerkleTx::GetDepthInMainChain(const CBlockIndex* &pindexRet) const bool CMerkleTx::IsLockedByInstantSend() const { - return instantsend.IsLockedInstantSendTransaction(GetHash()); + return instantsend.IsLockedInstantSendTransaction(GetHash()) || llmq::quorumInstantSendManager->IsLocked(GetHash()); } int CMerkleTx::GetBlocksToMaturity() const