From 0e33aa2a9d78024c34fb0f7c2aa1044c7d9f3f48 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Fri, 24 Apr 2026 15:43:33 +0700 Subject: [PATCH 1/2] revert: "fix: avoid improper dual way connection attempts" This reverts a57a8119a081bfa75f044e75b9c5eb073f0b58d5 from PR #6967. `relayMembers` and `connections` in EnsureQuorumConnections are not interchangeable. * `connections`: who this node should connect TO. For each pair (A, B) only the deterministic-outbound side is listed, so the pair results in one TCP connection. * `relayMembers`: who this node should ask to push recovered sigs. For every already-connected MN in the set we send QSENDRECSIGS=true, and the peer then flips m_wants_recsigs=true on its side. For the handshake to happen in both directions, the set must list every other quorum member -- not just the outbound half. After a57a811, only the outbound half is listed, so on the inbound half of each pair m_wants_recsigs stays false. RelayRecoveredSig only pushes QSIGREC to peers with m_wants_recsigs=true, so half of all proactive recovered-sig pushes are silently dropped. This only triggers with spork21 on (IsAllMembersConnectedEnabled returns true). In this case both the path that uses the half-mesh outbound subset and the path that relies on proactive QSIGREC. This fixes the functional test `feature_llmq_signing.py --spork21`, which times out in wait_for_sigs ~60% of the time while the non-spork21 variant passes on the same CI job. --- src/llmq/utils.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/llmq/utils.cpp b/src/llmq/utils.cpp index 41cab78da2f3..9648c3d009e9 100644 --- a/src/llmq/utils.cpp +++ b/src/llmq/utils.cpp @@ -801,13 +801,28 @@ bool EnsureQuorumConnections(const Consensus::LLMQParams& llmqParams, CConnman& util_params.m_base_index->GetBlockHash().ToString()); Uint256HashSet connections; + Uint256HashSet relayMembers; if (isMember) { connections = GetQuorumConnections(llmqParams, sporkman, util_params, myProTxHash, /*onlyOutbound=*/true); + // If all-members-connected is enabled for this quorum type, leverage the full-mesh + // connections for low-latency recovered sig propagation by treating all members as + // relay members (instead of the ring-based subset). This ensures peers will send + // QSENDRECSIGS to each other across the full mesh and set m_wants_recsigs widely. + if (IsAllMembersConnectedEnabled(llmqParams.type, sporkman)) { + for (const auto& dmn : members) { + if (dmn->proTxHash != myProTxHash) { + relayMembers.emplace(dmn->proTxHash); + } + } + } else { + relayMembers = GetQuorumRelayMembers(llmqParams, util_params, myProTxHash, true); + } } else { auto cindexes = CalcDeterministicWatchConnections(llmqParams.type, util_params.m_base_index, members.size(), 1); for (auto idx : cindexes) { connections.emplace(members[idx]->proTxHash); } + relayMembers = connections; } if (!connections.empty()) { if (!connman.HasMasternodeQuorumNodes(llmqParams.type, util_params.m_base_index->GetBlockHash()) && @@ -826,7 +841,9 @@ bool EnsureQuorumConnections(const Consensus::LLMQParams& llmqParams, CConnman& LogPrint(BCLog::NET_NETCONN, debugMsg.c_str()); /* Continued */ } connman.SetMasternodeQuorumNodes(llmqParams.type, util_params.m_base_index->GetBlockHash(), connections); - connman.SetMasternodeQuorumRelayMembers(llmqParams.type, util_params.m_base_index->GetBlockHash(), connections); + } + if (!relayMembers.empty()) { + connman.SetMasternodeQuorumRelayMembers(llmqParams.type, util_params.m_base_index->GetBlockHash(), relayMembers); } return true; } From c1cdb751e4b3576c884a733209d56bbf98524baa Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 30 Apr 2026 18:07:29 +0700 Subject: [PATCH 2/2] test: assert QSENDRECSIGS handshake is symmetric under spork21 If relayMembers gets conflated with the outbound-only connections set, one direction of each MN-MN pair never sends QSENDRECSIGS, the inbound side keeps m_wants_recsigs=false, and half of all proactive QSIGREC pushes are silently dropped which could be visible only as slow/flaky signing. --- test/functional/feature_llmq_signing.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/functional/feature_llmq_signing.py b/test/functional/feature_llmq_signing.py index bfe9094340bc..61db6c20942d 100755 --- a/test/functional/feature_llmq_signing.py +++ b/test/functional/feature_llmq_signing.py @@ -41,6 +41,7 @@ def run_test(self): if self.options.spork21: assert self.mninfo[0].get_node(self).getconnectioncount() == self.llmq_size + self.assert_qsendrecsigs_symmetric() id = "0000000000000000000000000000000000000000000000000000000000000001" msgHash = "0000000000000000000000000000000000000000000000000000000000000002" @@ -200,5 +201,25 @@ def assert_sigs_nochange(hasrecsigs, isconflicting1, isconflicting2, timeout): 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 + # m_wants_recsigs=false and silently drops half of all recsig pushes. + self.log.info("Assert QSENDRECSIGS was exchanged in both directions on every MN-MN edge") + mn_protxs = {mn.proTxHash for mn in self.mninfo} + + def all_symmetric(): + for mn in self.mninfo: + for p in mn.get_node(self).getpeerinfo(): + if p.get("verified_proregtx_hash", "") not in mn_protxs: + continue + if p.get("bytessent_per_msg", {}).get("qsendrecsigs", 0) == 0: + return False + if p.get("bytesrecv_per_msg", {}).get("qsendrecsigs", 0) == 0: + return False + return True + + self.wait_until(all_symmetric, timeout=10) + + if __name__ == '__main__': LLMQSigningTest().main()