diff --git a/src/active/context.cpp b/src/active/context.cpp index 831a1b4ccc67..e2c5c215d874 100644 --- a/src/active/context.cpp +++ b/src/active/context.cpp @@ -97,6 +97,14 @@ void ActiveContext::SetCJServer(gsl::not_null cj_server) m_cj_server = cj_server; } +void ActiveContext::InitializeCurrentBlockTip(const CBlockIndex* tip, bool ibd) +{ + UpdatedBlockTip(tip, nullptr, ibd); + if (tip) { + qman_handler->InitializeQuorumConnections(tip); + } +} + void ActiveContext::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) { if (fInitialDownload || pindexNew == pindexFork) // In IBD or blocks were disconnected without any new ones diff --git a/src/active/context.h b/src/active/context.h index c93690c4bc4a..2f45443d819c 100644 --- a/src/active/context.h +++ b/src/active/context.h @@ -74,6 +74,7 @@ struct ActiveContext final : public CValidationInterface { void Start(CConnman& connman, PeerManager& peerman, int16_t worker_count); void Stop(); + void InitializeCurrentBlockTip(const CBlockIndex* tip, bool ibd); CCoinJoinServer& GetCJServer() const; void SetCJServer(gsl::not_null cj_server); diff --git a/src/init.cpp b/src/init.cpp index db99f54a42a3..1a3656130f89 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -2417,6 +2417,15 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // but don't call it directly to prevent triggering of other listeners like zmq etc. // GetMainSignals().UpdatedBlockTip(::ChainActive().Tip()); g_ds_notification_interface->InitializeCurrentBlockTip(); + { + const CBlockIndex* tip = WITH_LOCK(::cs_main, return chainman.ActiveTip()); + const bool ibd = chainman.ActiveChainstate().IsInitialBlockDownload(); + if (node.observer_ctx && !node.active_ctx) { + node.observer_ctx->InitializeCurrentBlockTip(tip, ibd); + } + // Note: active_ctx initialization is deferred until after nodeman->Init() + // so that GetProTxHash() is available for quorum connection setup. + } { // Get all UTXOs for each MN collateral in one go so that we can fill coin cache early @@ -2481,6 +2490,13 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (node.active_ctx) { node.active_ctx->nodeman->Init(chainman.ActiveTip()); + // Initialize current block tip after nodeman->Init() so that + // GetProTxHash() is available for quorum connection setup. + // Without this ordering, EnsureQuorumConnections returns early + // because the null proTxHash makes the MN appear as a non-member. + const CBlockIndex* tip = WITH_LOCK(::cs_main, return chainman.ActiveTip()); + const bool ibd = chainman.ActiveChainstate().IsInitialBlockDownload(); + node.active_ctx->InitializeCurrentBlockTip(tip, ibd); } }); #ifdef ENABLE_WALLET diff --git a/src/llmq/observer/context.cpp b/src/llmq/observer/context.cpp index 717cd58f143b..00e4ce2ef784 100644 --- a/src/llmq/observer/context.cpp +++ b/src/llmq/observer/context.cpp @@ -47,6 +47,14 @@ void ObserverContext::Stop() qman_handler->Stop(); } +void ObserverContext::InitializeCurrentBlockTip(const CBlockIndex* tip, bool ibd) +{ + UpdatedBlockTip(tip, nullptr, ibd); + if (tip) { + qman_handler->InitializeQuorumConnections(tip); + } +} + void ObserverContext::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) { if (fInitialDownload || pindexNew == pindexFork) // In IBD or blocks were disconnected without any new ones diff --git a/src/llmq/observer/context.h b/src/llmq/observer/context.h index 0f6c2b5501cf..c373508dd386 100644 --- a/src/llmq/observer/context.h +++ b/src/llmq/observer/context.h @@ -49,6 +49,7 @@ struct ObserverContext final : public CValidationInterface { void Start(int16_t worker_count); void Stop(); + void InitializeCurrentBlockTip(const CBlockIndex* tip, bool ibd); protected: // CValidationInterface diff --git a/src/llmq/observer/quorums.cpp b/src/llmq/observer/quorums.cpp index 80084dd29ab5..69c727df9f47 100644 --- a/src/llmq/observer/quorums.cpp +++ b/src/llmq/observer/quorums.cpp @@ -59,6 +59,13 @@ void QuorumObserver::Stop() workerPool.stop(true); } +void QuorumObserver::InitializeQuorumConnections(gsl::not_null pindexNew) const +{ + for (const auto& params : Params().GetConsensus().llmqs) { + CheckQuorumConnections(params, pindexNew); + } +} + void QuorumObserver::UpdatedBlockTip(const CBlockIndex* pindexNew, bool fInitialDownload) const { if (!pindexNew) return; diff --git a/src/llmq/observer/quorums.h b/src/llmq/observer/quorums.h index 7b7bb68f79d4..6f60a734515d 100644 --- a/src/llmq/observer/quorums.h +++ b/src/llmq/observer/quorums.h @@ -96,6 +96,7 @@ class QuorumObserver void Stop(); void UpdatedBlockTip(const CBlockIndex* pindexNew, bool fInitialDownload) const; + void InitializeQuorumConnections(gsl::not_null pindexNew) const; public: virtual bool SetQuorumSecretKeyShare(CQuorum& quorum, Span skContributions) const; diff --git a/test/functional/p2p_instantsend.py b/test/functional/p2p_instantsend.py index b92d563f9abc..b6ff0a364809 100755 --- a/test/functional/p2p_instantsend.py +++ b/test/functional/p2p_instantsend.py @@ -4,7 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. from test_framework.test_framework import DashTestFramework -from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.util import assert_equal, assert_raises_rpc_error, force_finish_mnsync ''' p2p_instantsend.py @@ -36,6 +36,7 @@ def run_test(self): self.test_mempool_doublespend() self.test_block_doublespend() + self.test_instantsend_after_restart() def test_block_doublespend(self): sender = self.nodes[self.sender_idx] @@ -143,5 +144,85 @@ def test_mempool_doublespend(self): # mine more blocks self.generate(self.nodes[0], 2) + def test_instantsend_after_restart(self): + self.log.info("Testing InstantSend works after full restart without new blocks") + + # fund sender with confirmed coins + sender = self.nodes[self.sender_idx] + receiver = self.nodes[self.receiver_idx] + sender_addr = sender.getnewaddress() + fund_id = self.nodes[0].sendtoaddress(sender_addr, 1) + self.bump_mocktime(30) + self.sync_mempools() + for node in self.nodes: + self.wait_for_instantlock(fund_id, node) + tip = self.generate(self.nodes[0], 2)[-1] + self.bump_mocktime(30) + self.wait_for_chainlocked_block_all_nodes(tip) + self.sync_blocks() + assert sender.getbalance() >= 0.5 + + receiver_addr = receiver.getnewaddress() + + # restart all nodes without mining new blocks + self.log.info("Restarting all nodes") + num_simple_nodes = self.num_nodes - self.mn_count + self.stop_nodes() + + for i in range(num_simple_nodes): + self.start_node(i) + for mn_info in self.mninfo: + self.start_masternode(mn_info) + + # reconnect: simple nodes to node 0, MNs to node 0 only. + # Quorum connections between MNs must be re-established automatically + # via InitializeCurrentBlockTip → EnsureQuorumConnections, NOT via + # manual connect_nodes between MN pairs. + for i in range(1, num_simple_nodes): + self.connect_nodes(i, 0) + for mn_info in self.mninfo: + self.connect_nodes(mn_info.nodeIdx, 0) + for i in range(num_simple_nodes): + force_finish_mnsync(self.nodes[i]) + + # bump past WAIT_FOR_ISLOCK_TIMEOUT so txFirstSeenTime loss doesn't + # block chainlock signing for TXs mined before restart + self.bump_mocktime(10 * 60 + 1) + self.sync_blocks() + + # Verify that MNs formed quorum connections to other MNs after restart. + # InitializeCurrentBlockTip → EnsureQuorumConnections must populate + # masternodeQuorumNodes so ThreadOpenMasternodeConnections establishes + # MN-to-MN links beyond the manual connections to node 0. + self.log.info("Verifying MN-to-MN quorum connections formed after restart") + for mn_info in self.mninfo: + mn_node = self.nodes[mn_info.nodeIdx] + + def check_mn_peers(node=mn_node, my_hash=mn_info.proTxHash): + peers = node.getpeerinfo() + mn_peers = set(p['verified_proregtx_hash'] for p in peers + if p.get('verified_proregtx_hash', '') != '') + other_mn_peers = mn_peers - {my_hash} + return len(other_mn_peers) > 0 + self.wait_until(check_mn_peers, timeout=30) + + # re-grab references after restart + sender = self.nodes[self.sender_idx] + receiver = self.nodes[self.receiver_idx] + + # send a TX — needs IS lock from all restarted MNs, no new blocks mined + is_id = sender.sendtoaddress(receiver_addr, 0.5) + self.bump_mocktime(30) + self.sync_mempools() + for node in self.nodes: + self.wait_for_instantlock(is_id, node) + self.log.info("InstantSend lock succeeded after full restart") + + # clean up + receiver.sendtoaddress(self.nodes[0].getnewaddress(), 0.5, "", "", True) + self.bump_mocktime(30) + self.sync_mempools() + self.generate(self.nodes[0], 2) + if __name__ == '__main__': InstantSendTest().main()