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; } 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()