From d9fe92359d43aeb49b36b076818bedabd354e0a6 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:16:56 +0530 Subject: [PATCH 1/4] test: only call `quorum dkg{info,status}` on nodes that can see DKG --- test/functional/feature_llmq_rotation.py | 10 ++++------ test/functional/feature_llmq_simplepose.py | 2 +- test/functional/test_framework/test_framework.py | 8 ++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/test/functional/feature_llmq_rotation.py b/test/functional/feature_llmq_rotation.py index 496107490b58..72820d54eab7 100755 --- a/test/functional/feature_llmq_rotation.py +++ b/test/functional/feature_llmq_rotation.py @@ -72,8 +72,8 @@ def run_test(self): tip = self.nodes[0].getblockcount() next_dkg = 24 - (tip % 24) - for node in self.nodes: - dkg_info = node.quorum("dkginfo") + for mn in self.mninfo: + dkg_info = mn.get_node(self).quorum("dkginfo") assert_equal(dkg_info['active_dkgs'], 0) assert_equal(dkg_info['next_dkg'], next_dkg) @@ -94,10 +94,8 @@ def run_test(self): next_dkg = 24 - (tip % 24) assert next_dkg < 24 nonzero_dkgs = 0 - for i in range(len(self.nodes)): - dkg_info = self.nodes[i].quorum("dkginfo") - if i == 0: - assert_equal(dkg_info['active_dkgs'], 0) + for mn in self.mninfo: + dkg_info = mn.get_node(self).quorum("dkginfo") nonzero_dkgs += dkg_info['active_dkgs'] assert_equal(dkg_info['next_dkg'], next_dkg) assert_equal(nonzero_dkgs, 4) # 1 quorums 4 nodes diff --git a/test/functional/feature_llmq_simplepose.py b/test/functional/feature_llmq_simplepose.py index 0b3cfcb3953a..94b85bdb40fa 100755 --- a/test/functional/feature_llmq_simplepose.py +++ b/test/functional/feature_llmq_simplepose.py @@ -140,7 +140,7 @@ def mine_quorum_less_checks(self, expected_good_nodes, mninfos_online): self.wait_for_quorum_phase(q, 6, expected_good_nodes, None, 0, mninfos_online) self.log.info("Waiting final commitment") - self.wait_for_quorum_commitment(q, nodes) + self.wait_for_quorum_commitment(q, mninfos_online) self.log.info("Mining final commitment") self.bump_mocktime(1, nodes=nodes) diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index b2955cc8a1da..02a1260c2eeb 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -2065,10 +2065,10 @@ def check_dkg_session(): self.wait_until(check_dkg_session, timeout=timeout, sleep=sleep) - def wait_for_quorum_commitment(self, quorum_hash, nodes, llmq_type=100, timeout=15): + def wait_for_quorum_commitment(self, quorum_hash, mninfos, llmq_type=100, timeout=15): def check_dkg_comitments(): - for node in nodes: - s = node.quorum("dkgstatus") + for mn in mninfos: + s = mn.get_node(self).quorum("dkgstatus") if "minableCommitments" not in s: return False commits = s["minableCommitments"] @@ -2169,7 +2169,7 @@ def mine_quorum(self, llmq_type_name="llmq_test", llmq_type=100, expected_connec self.wait_for_quorum_phase(q, 6, expected_members, None, 0, mninfos_online, llmq_type_name=llmq_type_name) self.log.info("Waiting final commitment") - self.wait_for_quorum_commitment(q, nodes, llmq_type=llmq_type) + self.wait_for_quorum_commitment(q, mninfos_online, llmq_type=llmq_type) self.log.info("Mining final commitment") self.bump_mocktime(1) From 9d09f915c660f9bf38c8ee93c1f30eaebcb6cc5e Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Fri, 12 Dec 2025 20:11:18 +0530 Subject: [PATCH 2/4] rpc: add watch-only/masternode mode limits for `quorum dkg{info,status}` As we're isolating watch-only and masternode mode logic, we won't have access to the DKG debug object anymore, restrict access in preparation. --- src/llmq/core_write.cpp | 8 +++---- src/llmq/debug.h | 2 +- src/rpc/quorums.cpp | 47 ++++++++++++++++++++++------------------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/llmq/core_write.cpp b/src/llmq/core_write.cpp index 4bb3e9364bbe..89add66af10b 100644 --- a/src/llmq/core_write.cpp +++ b/src/llmq/core_write.cpp @@ -78,13 +78,13 @@ RPCResult CDKGDebugSessionStatus::GetJsonHelp(const std::string& key, bool optio } // CDKGDebugStatus::ToJson() defined in llmq/debug.cpp -RPCResult CDKGDebugStatus::GetJsonHelp(const std::string& key, bool optional) +RPCResult CDKGDebugStatus::GetJsonHelp(const std::string& key, bool optional, bool inner_optional) { return {RPCResult::Type::OBJ, key, optional, key.empty() ? "" : "The state of the node's DKG sessions", { - {RPCResult::Type::NUM, "time", "Adjusted time for the last update, timestamp"}, - {RPCResult::Type::STR, "timeStr", "Adjusted time for the last update, human friendly"}, - {RPCResult::Type::ARR, "session", "", { + {RPCResult::Type::NUM, "time", inner_optional, "Adjusted time for the last update, timestamp"}, + {RPCResult::Type::STR, "timeStr", inner_optional, "Adjusted time for the last update, human friendly"}, + {RPCResult::Type::ARR, "session", inner_optional, "", { {RPCResult::Type::OBJ, "", "", { {RPCResult::Type::NUM, "llmqType", "Name of quorum"}, GetRpcResult("quorumIndex"), diff --git a/src/llmq/debug.h b/src/llmq/debug.h index cebaadbee93f..f6923abbe9a4 100644 --- a/src/llmq/debug.h +++ b/src/llmq/debug.h @@ -92,7 +92,7 @@ class CDKGDebugStatus //std::map sessions; public: - [[nodiscard]] static RPCResult GetJsonHelp(const std::string& key, bool optional); + [[nodiscard]] static RPCResult GetJsonHelp(const std::string& key, bool optional, bool inner_optional = false); [[nodiscard]] UniValue ToJson(CDeterministicMNManager& dmnman, CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman, int detailLevel) const; }; diff --git a/src/rpc/quorums.cpp b/src/rpc/quorums.cpp index 210d81d5de8f..da30cfac4763 100644 --- a/src/rpc/quorums.cpp +++ b/src/rpc/quorums.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -296,7 +297,7 @@ static RPCHelpMan quorum_info() static RPCResult quorum_dkgstatus_help() { - auto ret = llmq::CDKGDebugStatus::GetJsonHelp(/*key=*/"", /*optional=*/false); + auto ret = llmq::CDKGDebugStatus::GetJsonHelp(/*key=*/"", /*optional=*/false, /*inner_optional=*/true); auto mod_inner = ret.m_inner; mod_inner.push_back({RPCResult::Type::ARR, "quorumConnections", "Array of objects containing quorum connection information", { {RPCResult::Type::OBJ, "", "", { @@ -332,12 +333,6 @@ static RPCHelpMan quorum_dkgstatus() RPCExamples{""}, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { - const NodeContext& node = EnsureAnyNodeContext(request.context); - const ChainstateManager& chainman = EnsureChainman(node); - const LLMQContext& llmq_ctx = EnsureLLMQContext(node); - const CConnman& connman = EnsureConnman(node); - CHECK_NONFATAL(node.sporkman); - int detailLevel = 0; if (!request.params[0].isNull()) { detailLevel = ParseInt32V(request.params[0], "detail_level"); @@ -346,17 +341,23 @@ static RPCHelpMan quorum_dkgstatus() } } - llmq::CDKGDebugStatus status; - llmq_ctx.dkg_debugman->GetLocalDebugStatus(status); - - auto ret = status.ToJson(*CHECK_NONFATAL(node.dmnman), *llmq_ctx.qsnapman, chainman, detailLevel); - - CBlockIndex* pindexTip = WITH_LOCK(cs_main, return chainman.ActiveChain().Tip()); - int tipHeight = pindexTip->nHeight; - const uint256 proTxHash = node.mn_activeman ? node.mn_activeman->GetProTxHash() : uint256(); - + UniValue ret(UniValue::VOBJ); UniValue minableCommitments(UniValue::VARR); UniValue quorumArrConnections(UniValue::VARR); + + const NodeContext& node = EnsureAnyNodeContext(request.context); + const ChainstateManager& chainman = EnsureChainman(node); + const LLMQContext& llmq_ctx = EnsureLLMQContext(node); + if (const auto* debugman = llmq_ctx.dkg_debugman.get(); debugman) { + llmq::CDKGDebugStatus status; + debugman->GetLocalDebugStatus(status); + ret = status.ToJson(*CHECK_NONFATAL(node.dmnman), *llmq_ctx.qsnapman, chainman, detailLevel); + } + + const CConnman& connman = EnsureConnman(node); + const CBlockIndex* const pindexTip = WITH_LOCK(cs_main, return chainman.ActiveChain().Tip()); + const int tipHeight = pindexTip->nHeight; + const uint256 proTxHash = node.mn_activeman ? node.mn_activeman->GetProTxHash() : uint256{}; for (const auto& type : llmq::GetEnabledQuorumTypes(chainman, pindexTip)) { const auto llmq_params_opt = Params().GetLLMQ(type); CHECK_NONFATAL(llmq_params_opt.has_value()); @@ -377,14 +378,14 @@ static RPCHelpMan quorum_dkgstatus() obj.pushKV("quorumHash", pQuorumBaseBlockIndex->GetBlockHash().ToString()); obj.pushKV("pindexTip", pindexTip->nHeight); - auto allConnections = llmq::utils::GetQuorumConnections(llmq_params, *node.sporkman, + auto allConnections = llmq::utils::GetQuorumConnections(llmq_params, *CHECK_NONFATAL(node.sporkman), {*node.dmnman, *llmq_ctx.qsnapman, chainman, pQuorumBaseBlockIndex}, - proTxHash, false); + proTxHash, /*onlyOutbound=*/false); auto outboundConnections = llmq::utils::GetQuorumConnections(llmq_params, *node.sporkman, {*node.dmnman, *llmq_ctx.qsnapman, chainman, pQuorumBaseBlockIndex}, - proTxHash, true); + proTxHash, /*onlyOutbound=*/true); std::map foundConnections; connman.ForEachNode([&](const CNode* pnode) { auto verifiedProRegTxHash = pnode->GetVerifiedProRegTxHash(); @@ -943,7 +944,6 @@ static RPCHelpMan quorum_rotationinfo() const NodeContext& node = EnsureAnyNodeContext(request.context); const ChainstateManager& chainman = EnsureChainman(node); const LLMQContext& llmq_ctx = EnsureLLMQContext(node); - ; llmq::CGetQuorumRotationInfo cmd; llmq::CQuorumRotationInfo quorumRotationInfoRet; @@ -997,14 +997,17 @@ static RPCHelpMan quorum_dkginfo() [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { const NodeContext& node = EnsureAnyNodeContext(request.context); - const ChainstateManager& chainman = EnsureChainman(node); - const LLMQContext& llmq_ctx = EnsureLLMQContext(node); + if (!node.active_ctx && !node.observer_ctx) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Only available in masternode or watch-only mode."); + } + const LLMQContext& llmq_ctx = EnsureLLMQContext(node); llmq::CDKGDebugStatus status; llmq_ctx.dkg_debugman->GetLocalDebugStatus(status); UniValue ret(UniValue::VOBJ); ret.pushKV("active_dkgs", status.sessions.size()); + const ChainstateManager& chainman = EnsureChainman(node); const int nTipHeight{WITH_LOCK(cs_main, return chainman.ActiveChain().Height())}; auto minNextDKG = [](const Consensus::Params& consensusParams, int nTipHeight) { int minDkgWindow{std::numeric_limits::max()}; From f5a16178854d7e3322e9d17c841e6c2823e896cf Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 9 Dec 2025 03:13:21 +0530 Subject: [PATCH 3/4] refactor: move `CDKGDebugManager` to `{Active,Observer}Context` --- src/init.cpp | 5 ++--- src/llmq/context.cpp | 2 -- src/llmq/context.h | 2 -- src/llmq/observer/context.cpp | 11 ++++++----- src/llmq/observer/context.h | 5 +++-- src/masternode/active/context.cpp | 6 ++++-- src/masternode/active/context.h | 2 ++ src/rpc/quorums.cpp | 8 +++++--- 8 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 9fe17a0bf06c..9f4b238a6718 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -2225,9 +2225,8 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) g_active_notification_interface = std::make_unique(*node.active_ctx, *node.mn_activeman); RegisterValidationInterface(g_active_notification_interface.get()); } else if (quorums_watch) { - node.observer_ctx = std::make_unique(*node.llmq_ctx->bls_worker, *node.dmnman, *node.mn_metaman, *node.llmq_ctx->dkg_debugman, - *node.llmq_ctx->quorum_block_processor, *node.llmq_ctx->qman, *node.llmq_ctx->qsnapman, chainman, - *node.sporkman, dash_db_params); + node.observer_ctx = std::make_unique(*node.llmq_ctx->bls_worker, *node.dmnman, *node.mn_metaman, *node.llmq_ctx->quorum_block_processor, + *node.llmq_ctx->qman, *node.llmq_ctx->qsnapman, chainman, *node.sporkman, dash_db_params); RegisterValidationInterface(node.observer_ctx.get()); } diff --git a/src/llmq/context.cpp b/src/llmq/context.cpp index 309f874fc21f..999ee67ddf5e 100644 --- a/src/llmq/context.cpp +++ b/src/llmq/context.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -20,7 +19,6 @@ LLMQContext::LLMQContext(ChainstateManager& chainman, CDeterministicMNManager& d const llmq::QvvecSyncModeMap& sync_map, const util::DbWrapperParams& db_params, bool quorums_recovery, bool quorums_watch, int8_t bls_threads, int64_t max_recsigs_age) : bls_worker{std::make_shared()}, - dkg_debugman{std::make_unique()}, qsnapman{std::make_unique(evo_db)}, quorum_block_processor{std::make_unique(chainman.ActiveChainstate(), dmnman, evo_db, *qsnapman, bls_threads)}, diff --git a/src/llmq/context.h b/src/llmq/context.h index a0cea150b1ee..4547a1062be9 100644 --- a/src/llmq/context.h +++ b/src/llmq/context.h @@ -21,7 +21,6 @@ class PeerManager; namespace llmq { class CChainLocksHandler; -class CDKGDebugManager; class CInstantSendManager; class CQuorumBlockProcessor; class CQuorumManager; @@ -58,7 +57,6 @@ struct LLMQContext { * but it still guarantees that objects are created and valid */ const std::shared_ptr bls_worker; - const std::unique_ptr dkg_debugman; const std::unique_ptr qsnapman; const std::unique_ptr quorum_block_processor; const std::unique_ptr qman; diff --git a/src/llmq/observer/context.cpp b/src/llmq/observer/context.cpp index 681f8f6eeaa7..241f764dabbc 100644 --- a/src/llmq/observer/context.cpp +++ b/src/llmq/observer/context.cpp @@ -4,19 +4,20 @@ #include +#include #include #include namespace llmq { -ObserverContext::ObserverContext(CBLSWorker& bls_worker, CDeterministicMNManager& dmnman, - CMasternodeMetaMan& mn_metaman, llmq::CDKGDebugManager& dkg_debugman, +ObserverContext::ObserverContext(CBLSWorker& bls_worker, CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_metaman, llmq::CQuorumBlockProcessor& qblockman, llmq::CQuorumManager& qman, llmq::CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman, const CSporkManager& sporkman, const util::DbWrapperParams& db_params) : m_qman{qman}, - qdkgsman{std::make_unique(bls_worker, dmnman, dkg_debugman, mn_metaman, qblockman, - qsnapman, /*mn_activeman=*/nullptr, chainman, sporkman, - db_params, /*quorums_watch=*/true)} + dkgdbgman{std::make_unique()}, + qdkgsman{std::make_unique(bls_worker, dmnman, *dkgdbgman, mn_metaman, qblockman, qsnapman, + /*mn_activeman=*/nullptr, chainman, sporkman, db_params, + /*quorums_watch=*/true)} { m_qman.ConnectManager(qdkgsman.get()); } diff --git a/src/llmq/observer/context.h b/src/llmq/observer/context.h index 86b03d2cb128..ab7e6ec98e6a 100644 --- a/src/llmq/observer/context.h +++ b/src/llmq/observer/context.h @@ -36,8 +36,8 @@ struct ObserverContext final : public CValidationInterface { ObserverContext(const ObserverContext&) = delete; ObserverContext& operator=(const ObserverContext&) = delete; ObserverContext(CBLSWorker& bls_worker, CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_metaman, - llmq::CDKGDebugManager& dkg_debugman, llmq::CQuorumBlockProcessor& qblockman, - llmq::CQuorumManager& qman, llmq::CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman, + llmq::CQuorumBlockProcessor& qblockman, llmq::CQuorumManager& qman, + llmq::CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman, const CSporkManager& sporkman, const util::DbWrapperParams& db_params); ~ObserverContext(); @@ -46,6 +46,7 @@ struct ObserverContext final : public CValidationInterface { void UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) override; public: + const std::unique_ptr dkgdbgman; const std::unique_ptr qdkgsman; }; } // namespace llmq diff --git a/src/masternode/active/context.cpp b/src/masternode/active/context.cpp index d737e3b74567..4e9a79904561 100644 --- a/src/masternode/active/context.cpp +++ b/src/masternode/active/context.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -25,8 +26,9 @@ ActiveContext::ActiveContext(CCoinJoinServer& cj_server, CConnman& connman, CDet m_llmq_ctx{llmq_ctx}, m_cj_server(cj_server), gov_signer{std::make_unique(connman, dmnman, govman, mn_activeman, chainman, mn_sync)}, - qdkgsman{std::make_unique(*llmq_ctx.bls_worker, dmnman, *llmq_ctx.dkg_debugman, - mn_metaman, *llmq_ctx.quorum_block_processor, *llmq_ctx.qsnapman, + dkgdbgman{std::make_unique()}, + qdkgsman{std::make_unique(*llmq_ctx.bls_worker, dmnman, *dkgdbgman, mn_metaman, + *llmq_ctx.quorum_block_processor, *llmq_ctx.qsnapman, &mn_activeman, chainman, sporkman, db_params, quorums_watch)}, shareman{std::make_unique(connman, chainman.ActiveChainstate(), *llmq_ctx.sigman, peerman, mn_activeman, *llmq_ctx.qman, sporkman)}, diff --git a/src/masternode/active/context.h b/src/masternode/active/context.h index c330730ace6f..aeb13de1be6e 100644 --- a/src/masternode/active/context.h +++ b/src/masternode/active/context.h @@ -28,6 +28,7 @@ namespace instantsend { class InstantSendSigner; } // namespace instantsend namespace llmq { +class CDKGDebugManager; class CDKGSessionManager; class CEHFSignalsHandler; class CSigSharesManager; @@ -63,6 +64,7 @@ struct ActiveContext { */ CCoinJoinServer& m_cj_server; const std::unique_ptr gov_signer; + const std::unique_ptr dkgdbgman; const std::unique_ptr qdkgsman; const std::unique_ptr shareman; const std::unique_ptr ehf_sighandler; diff --git a/src/rpc/quorums.cpp b/src/rpc/quorums.cpp index da30cfac4763..1fa468e1f48b 100644 --- a/src/rpc/quorums.cpp +++ b/src/rpc/quorums.cpp @@ -348,7 +348,9 @@ static RPCHelpMan quorum_dkgstatus() const NodeContext& node = EnsureAnyNodeContext(request.context); const ChainstateManager& chainman = EnsureChainman(node); const LLMQContext& llmq_ctx = EnsureLLMQContext(node); - if (const auto* debugman = llmq_ctx.dkg_debugman.get(); debugman) { + if (const auto* debugman = node.active_ctx ? node.active_ctx->dkgdbgman.get() + : node.observer_ctx ? node.observer_ctx->dkgdbgman.get() + : nullptr; debugman) { llmq::CDKGDebugStatus status; debugman->GetLocalDebugStatus(status); ret = status.ToJson(*CHECK_NONFATAL(node.dmnman), *llmq_ctx.qsnapman, chainman, detailLevel); @@ -1000,10 +1002,10 @@ static RPCHelpMan quorum_dkginfo() if (!node.active_ctx && !node.observer_ctx) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Only available in masternode or watch-only mode."); } + const auto& dkgdbgman = *(node.active_ctx ? node.active_ctx->dkgdbgman.get() : node.observer_ctx->dkgdbgman.get()); - const LLMQContext& llmq_ctx = EnsureLLMQContext(node); llmq::CDKGDebugStatus status; - llmq_ctx.dkg_debugman->GetLocalDebugStatus(status); + dkgdbgman.GetLocalDebugStatus(status); UniValue ret(UniValue::VOBJ); ret.pushKV("active_dkgs", status.sessions.size()); From e98fe4f880c740c8b9ac27abfcab1f046c869df1 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 23 Dec 2025 23:58:01 +0530 Subject: [PATCH 4/4] doc: add release notes for breaking changes --- doc/release-notes-7062.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 doc/release-notes-7062.md diff --git a/doc/release-notes-7062.md b/doc/release-notes-7062.md new file mode 100644 index 000000000000..5af5d34d8e2e --- /dev/null +++ b/doc/release-notes-7062.md @@ -0,0 +1,8 @@ +# RPC changes + +- `quorum dkginfo` will now require that nodes run either in watch-only mode (`-watchquorums`) or as an + active masternode (i.e. masternode mode) as regular nodes do not have insight into network DKG activity. + +- `quorum dkgstatus` will no longer emit the return values `time`, `timeStr` and `session` on nodes that + do not run in either watch-only or masternode mode as regular nodes do not have insight into network + DKG activity.