-
Notifications
You must be signed in to change notification settings - Fork 1.2k
test: segregate wallet-mode coverage and deduplicate functional test runs #7497
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 5 commits
b052e85
60f0c38
6980795
436bcfa
95996ba
850aeba
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 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -27,22 +27,18 @@ def set_test_params(self): | |||||||||
|
|
||||||||||
| def add_options(self, parser): | ||||||||||
| self.add_wallet_options(parser) | ||||||||||
| parser.add_argument("--spork21", dest="spork21", default=False, action="store_true", | ||||||||||
| help="Test with spork21 enabled") | ||||||||||
|
|
||||||||||
| def run_test(self): | ||||||||||
|
|
||||||||||
| # The first part of this test runs with spork21 off, the second part | ||||||||||
| # enables it mid-test and exercises the spork21-only paths on quorums | ||||||||||
| # mined after that. spork21 being active from the very first DKG is | ||||||||||
| # covered by feature_llmq_connections.py. | ||||||||||
|
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. 💬 Nitpick: Correct the stale first-DKG coverage citation
Suggested change
source: ['codex'] 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. Resolved in Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread. |
||||||||||
| self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0) | ||||||||||
| if self.options.spork21: | ||||||||||
| self.nodes[0].sporkupdate("SPORK_21_QUORUM_ALL_CONNECTED", 0) | ||||||||||
| self.wait_for_sporks_same() | ||||||||||
|
|
||||||||||
| self.mine_quorum() | ||||||||||
|
|
||||||||||
| if self.options.spork21: | ||||||||||
| assert self.mninfo[0].get_node(self).getconnectioncount() == self.llmq_size | ||||||||||
| self.assert_qsendrecsigs_symmetric() | ||||||||||
|
|
||||||||||
| id = "0000000000000000000000000000000000000000000000000000000000000001" | ||||||||||
| msgHash = "0000000000000000000000000000000000000000000000000000000000000002" | ||||||||||
| msgHashConflict = "0000000000000000000000000000000000000000000000000000000000000003" | ||||||||||
|
|
@@ -77,43 +73,12 @@ def assert_sigs_nochange(hasrecsigs, isconflicting1, isconflicting2, timeout): | |||||||||
| quorumHash = self.mninfo[1].get_node(self).quorum("selectquorum", q_type, id)["quorumHash"] | ||||||||||
| assert self.mninfo[1].get_node(self).quorum("sign", q_type, id, msgHash, quorumHash) | ||||||||||
| assert_sigs_nochange(False, False, False, 3) | ||||||||||
| # Sign third share and test optional submit parameter if spork21 is enabled, should result in recovered sig | ||||||||||
| # and conflict for msgHashConflict | ||||||||||
| if self.options.spork21: | ||||||||||
| # 1. Providing an invalid quorum hash and set submit=false, should throw an error | ||||||||||
| assert_raises_rpc_error(-8, 'quorum not found', self.mninfo[2].get_node(self).quorum, "sign", q_type, id, msgHash, id, False) | ||||||||||
| # 2. Providing a valid quorum hash and set submit=false, should return a valid sigShare object | ||||||||||
| sig_share_rpc_1 = self.mninfo[2].get_node(self).quorum("sign", q_type, id, msgHash, quorumHash, False) | ||||||||||
| sig_share_rpc_2 = self.mninfo[2].get_node(self).quorum("sign", q_type, id, msgHash, "", False) | ||||||||||
| assert_equal(sig_share_rpc_1, sig_share_rpc_2) | ||||||||||
| assert_sigs_nochange(False, False, False, 3) | ||||||||||
| # 3. Sending the sig share received from RPC to the recovery member through P2P interface, should result | ||||||||||
| # in a recovered sig | ||||||||||
| sig_share = CSigShare() | ||||||||||
| sig_share.llmqType = int(sig_share_rpc_1["llmqType"]) | ||||||||||
| sig_share.quorumHash = int(sig_share_rpc_1["quorumHash"], 16) | ||||||||||
| sig_share.quorumMember = int(sig_share_rpc_1["quorumMember"]) | ||||||||||
| sig_share.id = int(sig_share_rpc_1["id"], 16) | ||||||||||
| sig_share.msgHash = int(sig_share_rpc_1["msgHash"], 16) | ||||||||||
| sig_share.sigShare = bytes.fromhex(sig_share_rpc_1["signature"]) | ||||||||||
| for mn in self.mninfo: # type: MasternodeInfo | ||||||||||
| assert mn.get_node(self).getconnectioncount() == self.llmq_size | ||||||||||
| # Get the current recovery member of the quorum | ||||||||||
| q = self.nodes[0].quorum('selectquorum', q_type, id) | ||||||||||
| mn: MasternodeInfo = self.get_mninfo(q['recoveryMembers'][0]) | ||||||||||
| # Open a P2P connection to it | ||||||||||
| p2p_interface = mn.get_node(self).add_p2p_connection(P2PInterface()) | ||||||||||
| # Send the last required QSIGSHARE message to the recovery member | ||||||||||
| p2p_interface.send_message(msg_qsigshare([sig_share])) | ||||||||||
| else: | ||||||||||
| # If spork21 is not enabled just sign regularly | ||||||||||
| self.mninfo[2].get_node(self).quorum("sign", q_type, id, msgHash) | ||||||||||
| # Sign third share regularly, should result in recovered sig and conflict for msgHashConflict. | ||||||||||
| # The optional submit parameter is spork21-only and is exercised after spork21 is enabled below. | ||||||||||
| self.mninfo[2].get_node(self).quorum("sign", q_type, id, msgHash) | ||||||||||
|
|
||||||||||
| wait_for_sigs(True, False, True, 15) | ||||||||||
|
|
||||||||||
| if self.options.spork21: | ||||||||||
| mn.get_node(self).disconnect_p2ps() | ||||||||||
|
|
||||||||||
| # Test `quorum verify` rpc | ||||||||||
| node = self.mninfo[0].get_node(self) | ||||||||||
| recsig = node.quorum("getrecsig", q_type, id, msgHash) | ||||||||||
|
|
@@ -177,29 +142,74 @@ def assert_sigs_nochange(hasrecsigs, isconflicting1, isconflicting2, timeout): | |||||||||
| self.mninfo[i].get_node(self).quorum("sign", q_type, id, msgHash) | ||||||||||
| wait_for_sigs(True, False, True, 15) | ||||||||||
|
|
||||||||||
| if self.options.spork21: | ||||||||||
| id = uint256_to_string(request_id + 1) | ||||||||||
|
|
||||||||||
| # Isolate the node that is responsible for the recovery of a signature and assert that recovery fails | ||||||||||
| q = self.nodes[0].quorum('selectquorum', q_type, id) | ||||||||||
| mn: MasternodeInfo = self.get_mninfo(q['recoveryMembers'][0]) | ||||||||||
| mn.get_node(self).setnetworkactive(False) | ||||||||||
| self.wait_until(lambda: mn.get_node(self).getconnectioncount() == 0) | ||||||||||
| for i in range(4): | ||||||||||
| self.mninfo[i].get_node(self).quorum("sign", q_type, id, msgHash) | ||||||||||
| assert_sigs_nochange(False, False, False, 3) | ||||||||||
| # Need to re-connect so that it later gets the recovered sig | ||||||||||
| mn.get_node(self).setnetworkactive(True) | ||||||||||
| self.connect_nodes(mn.nodeIdx, 0) | ||||||||||
| force_finish_mnsync(mn.get_node(self)) | ||||||||||
| # Make sure intra-quorum connections were also restored | ||||||||||
| self.bump_mocktime(1) # need this to bypass quorum connection retry timeout | ||||||||||
| self.log.info("Enable SPORK_21_QUORUM_ALL_CONNECTED and mine one more quorum") | ||||||||||
| self.nodes[0].sporkupdate("SPORK_21_QUORUM_ALL_CONNECTED", 0) | ||||||||||
| self.wait_for_sporks_same() | ||||||||||
| self.mine_quorum() | ||||||||||
|
|
||||||||||
| for mn in self.mninfo: # type: MasternodeInfo | ||||||||||
| self.wait_until(lambda: mn.get_node(self).getconnectioncount() == self.llmq_size, timeout=10) | ||||||||||
| mn.get_node(self).ping() | ||||||||||
| self.wait_until(lambda: all('pingwait' not in peer for peer in mn.get_node(self).getpeerinfo())) | ||||||||||
| # Let 2 seconds pass so that the next node is used for recovery, which should succeed | ||||||||||
| self.bump_mocktime(2) | ||||||||||
| wait_for_sigs(True, False, True, 2) | ||||||||||
| self.assert_qsendrecsigs_symmetric() | ||||||||||
|
|
||||||||||
| self.log.info("Test the optional submit parameter and QSIGSHARE P2P submission") | ||||||||||
| id = uint256_to_string(request_id + 1) | ||||||||||
| # Sign first two shares regularly, using the optional quorumHash parameter for the second one | ||||||||||
| self.mninfo[0].get_node(self).quorum("sign", q_type, id, msgHash) | ||||||||||
| quorumHash = self.mninfo[1].get_node(self).quorum("selectquorum", q_type, id)["quorumHash"] | ||||||||||
| assert self.mninfo[1].get_node(self).quorum("sign", q_type, id, msgHash, quorumHash) | ||||||||||
| assert_sigs_nochange(False, False, False, 3) | ||||||||||
| # Sign third share and test the optional submit parameter, should result in recovered sig | ||||||||||
| # and conflict for msgHashConflict | ||||||||||
| # 1. Providing an invalid quorum hash and set submit=false, should throw an error | ||||||||||
| assert_raises_rpc_error(-8, 'quorum not found', self.mninfo[2].get_node(self).quorum, "sign", q_type, id, msgHash, id, False) | ||||||||||
| # 2. Providing a valid quorum hash and set submit=false, should return a valid sigShare object | ||||||||||
| sig_share_rpc_1 = self.mninfo[2].get_node(self).quorum("sign", q_type, id, msgHash, quorumHash, False) | ||||||||||
| sig_share_rpc_2 = self.mninfo[2].get_node(self).quorum("sign", q_type, id, msgHash, "", False) | ||||||||||
| assert_equal(sig_share_rpc_1, sig_share_rpc_2) | ||||||||||
| assert_sigs_nochange(False, False, False, 3) | ||||||||||
| # 3. Sending the sig share received from RPC to the recovery member through P2P interface, should result | ||||||||||
| # in a recovered sig | ||||||||||
| sig_share = CSigShare() | ||||||||||
| sig_share.llmqType = int(sig_share_rpc_1["llmqType"]) | ||||||||||
| sig_share.quorumHash = int(sig_share_rpc_1["quorumHash"], 16) | ||||||||||
| sig_share.quorumMember = int(sig_share_rpc_1["quorumMember"]) | ||||||||||
| sig_share.id = int(sig_share_rpc_1["id"], 16) | ||||||||||
| sig_share.msgHash = int(sig_share_rpc_1["msgHash"], 16) | ||||||||||
| sig_share.sigShare = bytes.fromhex(sig_share_rpc_1["signature"]) | ||||||||||
| # Get the current recovery member of the quorum | ||||||||||
| q = self.nodes[0].quorum('selectquorum', q_type, id) | ||||||||||
| mn: MasternodeInfo = self.get_mninfo(q['recoveryMembers'][0]) | ||||||||||
| # Open a P2P connection to it | ||||||||||
| p2p_interface = mn.get_node(self).add_p2p_connection(P2PInterface()) | ||||||||||
| # Send the last required QSIGSHARE message to the recovery member | ||||||||||
| p2p_interface.send_message(msg_qsigshare([sig_share])) | ||||||||||
|
|
||||||||||
| wait_for_sigs(True, False, True, 15) | ||||||||||
|
|
||||||||||
| mn.get_node(self).disconnect_p2ps() | ||||||||||
|
|
||||||||||
| id = uint256_to_string(request_id + 2) | ||||||||||
|
|
||||||||||
| # Isolate the node that is responsible for the recovery of a signature and assert that recovery fails | ||||||||||
| q = self.nodes[0].quorum('selectquorum', q_type, id) | ||||||||||
| mn = self.get_mninfo(q['recoveryMembers'][0]) | ||||||||||
| mn.get_node(self).setnetworkactive(False) | ||||||||||
| self.wait_until(lambda: mn.get_node(self).getconnectioncount() == 0) | ||||||||||
| for i in range(4): | ||||||||||
| self.mninfo[i].get_node(self).quorum("sign", q_type, id, msgHash) | ||||||||||
| assert_sigs_nochange(False, False, False, 3) | ||||||||||
| # Need to re-connect so that it later gets the recovered sig | ||||||||||
| mn.get_node(self).setnetworkactive(True) | ||||||||||
| self.connect_nodes(mn.nodeIdx, 0) | ||||||||||
| force_finish_mnsync(mn.get_node(self)) | ||||||||||
| # Make sure intra-quorum connections were also restored | ||||||||||
| self.bump_mocktime(1) # need this to bypass quorum connection retry timeout | ||||||||||
| self.wait_until(lambda: mn.get_node(self).getconnectioncount() == self.llmq_size, timeout=10) | ||||||||||
| mn.get_node(self).ping() | ||||||||||
| self.wait_until(lambda: all('pingwait' not in peer for peer in mn.get_node(self).getpeerinfo())) | ||||||||||
| # Let 2 seconds pass so that the next node is used for recovery, which should succeed | ||||||||||
| self.bump_mocktime(2) | ||||||||||
| wait_for_sigs(True, False, True, 2) | ||||||||||
|
|
||||||||||
| def assert_qsendrecsigs_symmetric(self): | ||||||||||
| # If only one direction's QSENDRECSIGS arrives, the receiving side keeps | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,10 +45,22 @@ def run_test(self): | |
| self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0) | ||
| self.wait_for_sporks_same() | ||
|
|
||
| # Lets isolate MNs one by one and verify that punishment/banning happens | ||
| self.test_banning(self.isolate_mn, 2) | ||
| if not self.options.disable_spork23: | ||
| # Lets isolate MNs one by one and verify that punishment/banning happens | ||
| self.test_banning(self.isolate_mn, 2) | ||
|
Comment on lines
+48
to
+50
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.
When AGENTS.md reference: AGENTS.md:L164-L175 Useful? React with 👍 / 👎. |
||
|
|
||
| self.repair_masternodes(False) | ||
| self.repair_masternodes(False) | ||
| else: | ||
| # The contribution-miss ban path (MarkBadMember -> PoSePunish) is not | ||
| # gated on spork23 (spork23 only gates connection/proto-version checks | ||
| # and probes), so it behaves identically with spork23 disabled and is | ||
| # already covered by the spork23-enabled run of this test. | ||
| self.log.info("Skipping contribution-miss banning, not affected by spork23") | ||
| # Mine one quorum in normal conditions so that the sections below start | ||
| # from the same state as in the spork23-enabled run: an existing quorum | ||
| # and all masternodes healthy. | ||
| self.reset_probe_timeouts() | ||
| self.mine_quorum() | ||
|
|
||
| self.nodes[0].sporkupdate("SPORK_21_QUORUM_ALL_CONNECTED", 0) | ||
| self.wait_for_sporks_same() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep a test where SPORK_21 is active before the first DKG: the cited replacement does not cover that scenario, because
feature_llmq_connections.pymines threellmq_testquorums at lines 36, 58, and 70 before enabling SPORK_21 at lines 74-76, then proceeds with a rotating quorum. Consequently, regressions specific to initializing or forming the first quorum with the spork already active are no longer detected after removing the--spork21variant.AGENTS.md reference: AGENTS.md:L164-L164
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right that
feature_llmq_connections.pywas the wrong citation — it mines threellmq_testquorums (lines 36, 58, 70) before enabling SPORK_21 at line 75, so it does not cover "spork21 active before the first DKG" for that quorum type. The PR description has been corrected.However, the permutation itself is still covered in every CI job:
feature_llmq_data_recovery.pyenablesSPORK_21_QUORUM_ALL_CONNECTEDat the very top ofrun_test(line 164), on a fresh chain before its firstmine_quorum()(line 173), and then forms bothllmq_testandllmq_test_v17quorums every DKG cycle with the spork active from before the first DKG ever. That exercises first-quorum formation under all-connected topology at least as thoroughly as the removed--spork21variant did (two quorum types concurrently vs one). Additionally,feature_llmq_connections.pydoes cover the chain's first rotation (dip0024) DKGs under spork21 viamine_cycle_quorum()after the spork is enabled.The only delta vs the removed variant is quorum size (5 members there vs 4/3 in data_recovery); the spork21 path (
EnsureQuorumConnectionsall-connected + probes) has no first-DKG-specific branching that depends on member count, so we are not restoring the variant.