-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: initialize quorum connections on startup #7240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5e79dbe
c25c61a
ecd5c90
5a43003
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -97,6 +97,14 @@ void ActiveContext::SetCJServer(gsl::not_null<CCoinJoinServer*> 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); | ||
| } | ||
| } | ||
|
Comment on lines
+100
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Identical InitializeCurrentBlockTip bodies in ActiveContext and ObserverContext The method body is identical in ActiveContext (lines 100-106) and ObserverContext (src/llmq/observer/context.cpp:50-56): both call UpdatedBlockTip then qman_handler->InitializeQuorumConnections. The differentiated behavior comes from two layers: (1) UpdatedBlockTip itself differs — ActiveContext also calls nodeman, ehf_sighandler, gov_signer — and (2) InitializeQuorumConnections dispatches through the virtual CheckQuorumConnections (QuorumParticipant overrides QuorumObserver). Both classes are source: ['claude'] 🤖 Fix this with AI agents |
||
|
|
||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,13 @@ void QuorumObserver::Stop() | |
| workerPool.stop(true); | ||
| } | ||
|
|
||
| void QuorumObserver::InitializeQuorumConnections(gsl::not_null<const CBlockIndex*> pindexNew) const | ||
| { | ||
| for (const auto& params : Params().GetConsensus().llmqs) { | ||
| CheckQuorumConnections(params, pindexNew); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR descriptions says that:
But
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For the common case this patch targets — an up-to-date masternode that just restarted on an idle chain — the tip is current, so the quorum node maps are correct by the time For the rare case of a masternode that's far behind, the maps could potentially contain stale entries. There's a brief window where
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is still a bit confusing for me, but I guess it's much better than broken |
||
| } | ||
| } | ||
|
|
||
| void QuorumObserver::UpdatedBlockTip(const CBlockIndex* pindexNew, bool fInitialDownload) const | ||
| { | ||
| if (!pindexNew) return; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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") | ||
|
Comment on lines
+215
to
+219
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should drop bump_mocktime/sync-mempools when https://github.com/dashpay/dash/pull/7241/files is merged |
||
|
|
||
| # clean up | ||
| receiver.sendtoaddress(self.nodes[0].getnewaddress(), 0.5, "", "", True) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: instead "", "" consider using named arguments here |
||
| self.bump_mocktime(30) | ||
| self.sync_mempools() | ||
| self.generate(self.nodes[0], 2) | ||
|
|
||
| if __name__ == '__main__': | ||
| InstantSendTest().main() | ||
Uh oh!
There was an error while loading. Please reload this page.