Line data Source code
1 : // Copyright (c) 2017-2025 The Dash Core developers
2 : // Distributed under the MIT software license, see the accompanying
3 : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 :
5 : #include <active/context.h>
6 : #include <active/masternode.h>
7 : #include <chainlock/chainlock.h>
8 : #include <chainlock/clsig.h>
9 : #include <chainlock/handler.h>
10 : #include <evo/deterministicmns.h>
11 : #include <llmq/blockprocessor.h>
12 : #include <llmq/commitment.h>
13 : #include <llmq/context.h>
14 : #include <llmq/debug.h>
15 : #include <llmq/dkgsession.h>
16 : #include <llmq/observer.h>
17 : #include <llmq/options.h>
18 : #include <llmq/quorumsman.h>
19 : #include <llmq/signhash.h>
20 : #include <llmq/signing.h>
21 : #include <llmq/signing_shares.h>
22 : #include <llmq/snapshot.h>
23 : #include <llmq/utils.h>
24 : #include <rpc/evo_util.h>
25 : #include <util/helpers.h>
26 :
27 : #include <chainparams.h>
28 : #include <core_io.h>
29 : #include <deploymentstatus.h>
30 : #include <index/txindex.h>
31 : #include <net_processing.h>
32 : #include <netmessagemaker.h>
33 : #include <node/context.h>
34 : #include <rpc/server.h>
35 : #include <rpc/server_util.h>
36 : #include <rpc/util.h>
37 : #include <util/check.h>
38 : #include <validation.h>
39 :
40 : #include <iomanip>
41 : #include <optional>
42 :
43 : using node::GetTransaction;
44 : using node::NodeContext;
45 :
46 92 : static RPCHelpMan quorum_list()
47 : {
48 184 : return RPCHelpMan{"quorum list",
49 92 : "List of on-chain quorums\n",
50 184 : {
51 92 : {"count", RPCArg::Type::NUM, RPCArg::DefaultHint{"The active quorum count if not specified"},
52 92 : "Number of quorums to list.\n"
53 : "Can be CPU/disk heavy when the value is larger than the number of active quorums."
54 : },
55 : },
56 92 : RPCResult{
57 92 : RPCResult::Type::OBJ, "", "",
58 184 : {
59 184 : {RPCResult::Type::ARR, "quorumName", "List of quorum hashes per some quorum type",
60 184 : {
61 92 : {RPCResult::Type::STR_HEX, "quorumHash", "Quorum hash. Note: most recent quorums come first."},
62 : }},
63 : }},
64 92 : RPCExamples{
65 92 : HelpExampleCli("quorum", "list")
66 92 : + HelpExampleCli("quorum", "list 10")
67 92 : + HelpExampleRpc("quorum", "list, 10")
68 : },
69 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
70 : {
71 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
72 0 : const ChainstateManager& chainman = EnsureChainman(node);
73 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
74 :
75 0 : int count = -1;
76 0 : if (!request.params[0].isNull()) {
77 0 : count = request.params[0].getInt<int>();
78 0 : if (count < -1) {
79 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "count can't be negative");
80 : }
81 0 : }
82 :
83 0 : UniValue ret(UniValue::VOBJ);
84 :
85 0 : CBlockIndex* pindexTip = WITH_LOCK(cs_main, return chainman.ActiveChain().Tip());
86 :
87 0 : for (const auto& type : llmq::GetEnabledQuorumTypes(chainman, pindexTip)) {
88 0 : const auto llmq_params_opt = Params().GetLLMQ(type);
89 0 : CHECK_NONFATAL(llmq_params_opt.has_value());
90 0 : UniValue v(UniValue::VARR);
91 :
92 0 : auto quorums = llmq_ctx.qman->ScanQuorums(type, pindexTip, count > -1 ? count : llmq_params_opt->signingActiveQuorumCount);
93 0 : for (const auto& q : quorums) {
94 0 : v.push_back(q->qc->quorumHash.ToString());
95 : }
96 :
97 0 : ret.pushKV(std::string(llmq_params_opt->name), v);
98 0 : }
99 :
100 0 : return ret;
101 0 : },
102 : };
103 0 : }
104 :
105 92 : static RPCHelpMan quorum_list_extended()
106 : {
107 184 : return RPCHelpMan{"quorum listextended",
108 92 : "Extended list of on-chain quorums\n",
109 184 : {
110 92 : {"height", RPCArg::Type::NUM, RPCArg::DefaultHint{"Tip height if not specified"}, "Active quorums at the height."},
111 : },
112 92 : RPCResult{
113 92 : RPCResult::Type::OBJ, "", "",
114 184 : {
115 184 : {RPCResult::Type::ARR, "quorumName", "List of quorum details per quorum type",
116 184 : {
117 184 : {RPCResult::Type::OBJ, "", "",
118 184 : {
119 184 : {RPCResult::Type::OBJ, "xxxx", "Quorum hash. Note: most recent quorums come first.",
120 552 : {
121 92 : {RPCResult::Type::NUM, "creationHeight", "Block height where the DKG started."},
122 92 : {RPCResult::Type::NUM, "quorumIndex", "Quorum index (applicable only to rotated quorums)."},
123 92 : {RPCResult::Type::STR_HEX, "minedBlockHash", "Blockhash where the commitment was mined."},
124 92 : {RPCResult::Type::NUM, "numValidMembers", "The total of valid members."},
125 92 : {RPCResult::Type::STR_AMOUNT, "healthRatio", "The ratio of healthy members to quorum size. Range [0.0 - 1.0]."}
126 : }}
127 : }}
128 : }}
129 : }},
130 92 : RPCExamples{
131 92 : HelpExampleCli("quorum", "listextended")
132 92 : + HelpExampleCli("quorum", "listextended 2500")
133 92 : + HelpExampleRpc("quorum", "listextended, 2500")
134 : },
135 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
136 : {
137 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
138 0 : const ChainstateManager& chainman = EnsureChainman(node);
139 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
140 :
141 0 : int nHeight = -1;
142 0 : if (!request.params[0].isNull()) {
143 0 : nHeight = request.params[0].getInt<int>();
144 0 : if (nHeight < 0 || nHeight > WITH_LOCK(cs_main, return chainman.ActiveChain().Height())) {
145 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range");
146 : }
147 0 : }
148 :
149 0 : UniValue ret(UniValue::VOBJ);
150 :
151 0 : CBlockIndex* pblockindex = nHeight != -1 ? WITH_LOCK(cs_main, return chainman.ActiveChain()[nHeight]) : WITH_LOCK(cs_main, return chainman.ActiveChain().Tip());
152 :
153 0 : for (const auto& type : llmq::GetEnabledQuorumTypes(chainman, pblockindex)) {
154 0 : const auto llmq_params_opt = Params().GetLLMQ(type);
155 0 : CHECK_NONFATAL(llmq_params_opt.has_value());
156 0 : const auto& llmq_params = llmq_params_opt.value();
157 0 : UniValue v(UniValue::VARR);
158 :
159 0 : auto quorums = llmq_ctx.qman->ScanQuorums(type, pblockindex, llmq_params.signingActiveQuorumCount);
160 0 : for (const auto& q : quorums) {
161 0 : size_t num_members = q->members.size();
162 0 : size_t num_valid_members = std::count_if(q->qc->validMembers.begin(), q->qc->validMembers.begin() + num_members, [](auto val){return val;});
163 0 : double health_ratio = num_members > 0 ? double(num_valid_members) / double(num_members) : 0.0;
164 0 : std::stringstream ss;
165 0 : ss << std::fixed << std::setprecision(2) << health_ratio;
166 0 : UniValue obj(UniValue::VOBJ);
167 : {
168 0 : UniValue j(UniValue::VOBJ);
169 0 : if (llmq_params.useRotation) {
170 0 : j.pushKV("quorumIndex", q->qc->quorumIndex);
171 0 : }
172 0 : j.pushKV("creationHeight", q->m_quorum_base_block_index->nHeight);
173 0 : j.pushKV("minedBlockHash", q->minedBlockHash.ToString());
174 0 : j.pushKV("numValidMembers", num_valid_members);
175 0 : j.pushKV("healthRatio", ss.str());
176 0 : obj.pushKV(q->qc->quorumHash.ToString(),j);
177 0 : }
178 0 : v.push_back(obj);
179 0 : }
180 0 : ret.pushKV(std::string(llmq_params.name), v);
181 0 : }
182 :
183 0 : return ret;
184 0 : },
185 : };
186 0 : }
187 :
188 0 : static UniValue BuildQuorumInfo(const llmq::CQuorumBlockProcessor& quorum_block_processor,
189 : const llmq::CQuorum& quorum, bool includeMembers, bool includeSkShare)
190 : {
191 0 : UniValue ret(UniValue::VOBJ);
192 :
193 0 : ret.pushKV("height", quorum.m_quorum_base_block_index->nHeight);
194 0 : ret.pushKV("type", std::string(quorum.params.name));
195 0 : ret.pushKV("quorumHash", quorum.qc->quorumHash.ToString());
196 0 : ret.pushKV("quorumIndex", quorum.qc->quorumIndex);
197 0 : ret.pushKV("minedBlock", quorum.minedBlockHash.ToString());
198 :
199 0 : if (quorum.params.useRotation) {
200 0 : auto previousActiveCommitment = quorum_block_processor.GetLastMinedCommitmentsByQuorumIndexUntilBlock(quorum.params.type, quorum.m_quorum_base_block_index, quorum.qc->quorumIndex, 0);
201 0 : if (previousActiveCommitment.has_value()) {
202 0 : int previousConsecutiveDKGFailures = (quorum.m_quorum_base_block_index->nHeight - previousActiveCommitment.value()->nHeight) / quorum.params.dkgInterval - 1;
203 0 : ret.pushKV("previousConsecutiveDKGFailures", previousConsecutiveDKGFailures);
204 0 : }
205 : else {
206 0 : ret.pushKV("previousConsecutiveDKGFailures", 0);
207 : }
208 0 : }
209 :
210 0 : if (includeMembers) {
211 0 : UniValue membersArr(UniValue::VARR);
212 0 : for (size_t i = 0; i < quorum.members.size(); i++) {
213 0 : const auto& dmn = quorum.members[i];
214 0 : UniValue mo(UniValue::VOBJ);
215 0 : mo.pushKV("proTxHash", dmn->proTxHash.ToString());
216 0 : if (IsDeprecatedRPCEnabled("service")) {
217 0 : mo.pushKV("service", dmn->pdmnState->netInfo->GetPrimary().ToStringAddrPort());
218 0 : }
219 0 : mo.pushKV("addresses", GetNetInfoWithLegacyFields(*dmn->pdmnState, dmn->nType));
220 0 : mo.pushKV("pubKeyOperator", dmn->pdmnState->pubKeyOperator.ToString());
221 0 : mo.pushKV("valid", static_cast<bool>(quorum.qc->validMembers[i]));
222 0 : if (quorum.qc->validMembers[i]) {
223 0 : if (quorum.params.is_single_member()) {
224 0 : mo.pushKV("pubKeyShare", dmn->pdmnState->pubKeyOperator.ToString());
225 0 : } else {
226 0 : CBLSPublicKey pubKey = quorum.GetPubKeyShare(i);
227 0 : if (pubKey.IsValid()) {
228 0 : mo.pushKV("pubKeyShare", pubKey.ToString());
229 0 : }
230 : }
231 0 : }
232 0 : membersArr.push_back(mo);
233 0 : }
234 :
235 0 : ret.pushKV("members", membersArr);
236 0 : }
237 0 : ret.pushKV("quorumPublicKey", quorum.qc->quorumPublicKey.ToString());
238 0 : const CBLSSecretKey& skShare = quorum.GetSkShare();
239 0 : if (includeSkShare && skShare.IsValid()) {
240 0 : ret.pushKV("secretKeyShare", skShare.ToString());
241 0 : }
242 0 : return ret;
243 0 : }
244 :
245 92 : static RPCHelpMan quorum_info()
246 : {
247 184 : return RPCHelpMan{"quorum info",
248 92 : "Return information about a quorum\n",
249 368 : {
250 92 : {"llmqType", RPCArg::Type::NUM, RPCArg::Optional::NO, "LLMQ type."},
251 92 : {"quorumHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Block hash of quorum."},
252 92 : {"includeSkShare", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include secret key share in output."},
253 : },
254 92 : RPCResult{
255 92 : RPCResult::Type::OBJ, "", "",
256 736 : {
257 92 : {RPCResult::Type::NUM, "height", "Quorum Height"},
258 92 : {RPCResult::Type::STR, "type", "Quorum type"},
259 92 : GetRpcResult("quorumHash"),
260 92 : GetRpcResult("quorumIndex"),
261 92 : {RPCResult::Type::STR_HEX, "minedBlock", "Blockhash where the commitment was mined."},
262 92 : {RPCResult::Type::NUM, "previousConsecutiveDKGFailures", "Number of previous consecutive DKG failures. Only present for rotation-enabled quorums."},
263 184 : {RPCResult::Type::ARR, "members", "Members of quorum",
264 184 : {
265 184 : {RPCResult::Type::OBJ, "", "",
266 644 : {
267 92 : GetRpcResult("proTxHash"),
268 92 : GetRpcResult("service", /*optional=*/true),
269 92 : GetRpcResult("addresses"),
270 92 : GetRpcResult("pubKeyOperator"),
271 92 : {RPCResult::Type::BOOL, "valid", "True if member is valid for this DKG"},
272 92 : {RPCResult::Type::STR_HEX, "pubKeyShare", /*optional=*/true, "Share of BLS public key of the member. Only present if member is valid."}
273 : }},
274 : },
275 : },
276 : },
277 : },
278 92 : RPCExamples{""},
279 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
280 : {
281 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
282 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
283 :
284 0 : const Consensus::LLMQType llmqType{static_cast<Consensus::LLMQType>(request.params[0].getInt<int>())};
285 0 : if (!Params().GetLLMQ(llmqType).has_value()) {
286 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid LLMQ type");
287 : }
288 :
289 0 : const uint256 quorumHash(ParseHashV(request.params[1], "quorumHash"));
290 0 : bool includeSkShare = false;
291 0 : if (!request.params[2].isNull()) {
292 0 : includeSkShare = ParseBoolV(request.params[2], "includeSkShare");
293 0 : }
294 :
295 0 : const auto quorum = llmq_ctx.qman->GetQuorum(llmqType, quorumHash);
296 0 : if (!quorum) {
297 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "quorum not found");
298 : }
299 :
300 0 : return BuildQuorumInfo(*llmq_ctx.quorum_block_processor, *quorum, true, includeSkShare);
301 0 : },
302 : };
303 0 : }
304 :
305 92 : static RPCResult quorum_dkgstatus_help()
306 : {
307 92 : auto ret = llmq::CDKGDebugManager::GetJsonHelp(/*key=*/"", /*optional=*/false, /*inner_optional=*/true);
308 92 : auto mod_inner = ret.m_inner;
309 184 : mod_inner.push_back({RPCResult::Type::ARR, "quorumConnections", "Array of objects containing quorum connection information", {
310 644 : {RPCResult::Type::OBJ, "", "", {
311 92 : GetRpcResult("llmqType"),
312 92 : GetRpcResult("quorumIndex"),
313 92 : {RPCResult::Type::NUM, "pQuorumBaseBlockIndex", /*optional=*/true, "Height of the quorum’s base block"},
314 92 : GetRpcResult("quorumHash", /*optional=*/true),
315 92 : {RPCResult::Type::NUM, "pindexTip", /*optional=*/true, "Height of the quorum index tip"},
316 184 : {RPCResult::Type::ARR, "quorumConnections", /*optional=*/true, "", {
317 460 : {RPCResult::Type::OBJ, "", "", {
318 92 : GetRpcResult("proTxHash"),
319 92 : {RPCResult::Type::BOOL, "connected", "Returns true if connection is active"},
320 92 : {RPCResult::Type::STR, "address", /*optional=*/true, "IP address and port of the masternode"},
321 92 : {RPCResult::Type::BOOL, "outbound", /*optional=*/true, "Returns true if outbound connection"},
322 : }}}}
323 : }}}});
324 184 : mod_inner.push_back({RPCResult::Type::ARR, "minableCommitments", "Array of objects containing minable commitments", {
325 92 : llmq::CFinalCommitment::GetJsonHelp(/*key=*/"", /*optional=*/false)}});
326 92 : return RPCResult{ret.m_type, ret.m_key_name, ret.m_description, mod_inner};
327 92 : }
328 :
329 92 : static RPCHelpMan quorum_dkgstatus()
330 : {
331 184 : return RPCHelpMan{"quorum dkgstatus",
332 92 : "Return the status of the current DKG process.\n"
333 : "Works only when SPORK_17_QUORUM_DKG_ENABLED spork is ON.\n",
334 184 : {
335 92 : {"detail_level", RPCArg::Type::NUM, RPCArg::Default{0},
336 92 : "Detail level of output.\n"
337 : "0=Only show counts. 1=Show member indexes. 2=Show member's ProTxHashes."},
338 : },
339 92 : quorum_dkgstatus_help(),
340 92 : RPCExamples{""},
341 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
342 : {
343 0 : int detailLevel = 0;
344 0 : if (!request.params[0].isNull()) {
345 0 : detailLevel = request.params[0].getInt<int>();
346 0 : if (detailLevel < 0 || detailLevel > 2) {
347 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid detail_level");
348 : }
349 0 : }
350 :
351 0 : UniValue ret(UniValue::VOBJ);
352 0 : UniValue minableCommitments(UniValue::VARR);
353 0 : UniValue quorumArrConnections(UniValue::VARR);
354 :
355 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
356 0 : if (const auto* debugman = node.active_ctx ? node.active_ctx->dkgdbgman.get()
357 0 : : node.observer_ctx ? node.observer_ctx->dkgdbgman.get()
358 0 : : nullptr; debugman) {
359 0 : ret = debugman->ToJson(detailLevel);
360 0 : }
361 :
362 0 : const CConnman& connman = EnsureConnman(node);
363 0 : const ChainstateManager& chainman = EnsureChainman(node);
364 0 : const CBlockIndex* const pindexTip = WITH_LOCK(cs_main, return chainman.ActiveChain().Tip());
365 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
366 0 : const int tipHeight = pindexTip->nHeight;
367 0 : const uint256 proTxHash = node.active_ctx ? node.active_ctx->nodeman->GetProTxHash() : uint256{};
368 0 : for (const auto& type : llmq::GetEnabledQuorumTypes(chainman, pindexTip)) {
369 0 : const auto llmq_params_opt = Params().GetLLMQ(type);
370 0 : CHECK_NONFATAL(llmq_params_opt.has_value());
371 0 : const auto& llmq_params = llmq_params_opt.value();
372 0 : bool rotation_enabled = llmq::IsQuorumRotationEnabled(llmq_params, pindexTip);
373 0 : int quorums_num = rotation_enabled ? llmq_params.signingActiveQuorumCount : 1;
374 :
375 0 : for (const int quorumIndex : util::irange(quorums_num)) {
376 0 : UniValue obj(UniValue::VOBJ);
377 0 : obj.pushKV("llmqType", std::string(llmq_params.name));
378 0 : obj.pushKV("quorumIndex", quorumIndex);
379 :
380 0 : if (node.active_ctx) {
381 0 : int quorumHeight = tipHeight - (tipHeight % llmq_params.dkgInterval) + quorumIndex;
382 0 : if (quorumHeight <= tipHeight) {
383 0 : const CBlockIndex* pQuorumBaseBlockIndex = WITH_LOCK(cs_main, return chainman.ActiveChain()[quorumHeight]);
384 0 : obj.pushKV("pQuorumBaseBlockIndex", pQuorumBaseBlockIndex->nHeight);
385 0 : obj.pushKV("quorumHash", pQuorumBaseBlockIndex->GetBlockHash().ToString());
386 0 : obj.pushKV("pindexTip", pindexTip->nHeight);
387 :
388 0 : auto allConnections = llmq::utils::GetQuorumConnections(llmq_params, *CHECK_NONFATAL(node.sporkman),
389 0 : {*node.dmnman, *llmq_ctx.qsnapman, chainman,
390 0 : pQuorumBaseBlockIndex},
391 : proTxHash, /*onlyOutbound=*/false);
392 0 : auto outboundConnections = llmq::utils::GetQuorumConnections(llmq_params, *node.sporkman,
393 0 : {*node.dmnman, *llmq_ctx.qsnapman,
394 0 : chainman, pQuorumBaseBlockIndex},
395 : proTxHash, /*onlyOutbound=*/true);
396 0 : std::map<uint256, CAddress> foundConnections;
397 0 : connman.ForEachNode([&](const CNode* pnode) {
398 0 : auto verifiedProRegTxHash = pnode->GetVerifiedProRegTxHash();
399 0 : if (!verifiedProRegTxHash.IsNull() && allConnections.count(verifiedProRegTxHash)) {
400 0 : foundConnections.emplace(verifiedProRegTxHash, pnode->addr);
401 0 : }
402 0 : });
403 0 : UniValue arr(UniValue::VARR);
404 0 : for (const auto& ec : allConnections) {
405 0 : UniValue ecj(UniValue::VOBJ);
406 0 : ecj.pushKV("proTxHash", ec.ToString());
407 0 : if (foundConnections.count(ec)) {
408 0 : ecj.pushKV("connected", true);
409 0 : ecj.pushKV("address", foundConnections[ec].ToStringAddrPort());
410 0 : } else {
411 0 : ecj.pushKV("connected", false);
412 : }
413 0 : ecj.pushKV("outbound", outboundConnections.count(ec) != 0);
414 0 : arr.push_back(ecj);
415 0 : }
416 0 : obj.pushKV("quorumConnections", arr);
417 0 : }
418 0 : }
419 0 : quorumArrConnections.push_back(obj);
420 0 : }
421 :
422 0 : LOCK(cs_main);
423 0 : std::optional<std::vector<llmq::CFinalCommitment>> vfqc = llmq_ctx.quorum_block_processor->GetMineableCommitments(llmq_params, tipHeight);
424 0 : if (vfqc.has_value()) {
425 0 : for (const auto& fqc : vfqc.value()) {
426 0 : minableCommitments.push_back(fqc.ToJson());
427 : }
428 0 : }
429 0 : }
430 0 : ret.pushKV("quorumConnections", quorumArrConnections);
431 0 : ret.pushKV("minableCommitments", minableCommitments);
432 0 : return ret;
433 0 : },
434 : };
435 0 : }
436 :
437 92 : static RPCHelpMan quorum_memberof()
438 : {
439 184 : return RPCHelpMan{"quorum memberof",
440 92 : "Checks which quorums the given masternode is a member of.\n",
441 276 : {
442 92 : {"proTxHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "ProTxHash of the masternode."},
443 92 : {"scanQuorumsCount", RPCArg::Type::NUM, RPCArg::DefaultHint{"The active quorum count for each specific quorum type is used"},
444 92 : "Number of quorums to scan for.\n"
445 : "Can be CPU/disk heavy when the value is larger than the number of active quorums."
446 : },
447 : },
448 92 : RPCResult{
449 92 : RPCResult::Type::ARR, "quorums", "",
450 184 : {
451 184 : {RPCResult::Type::OBJ, "", "Quorum Info",
452 368 : {
453 92 : {RPCResult::Type::ELISION, "", "See `help quorum info` for details"},
454 92 : {RPCResult::Type::BOOL, "isValidMember", ""},
455 92 : {RPCResult::Type::NUM, "memberIndex", ""},
456 : }},
457 : },
458 : },
459 92 : RPCExamples{""},
460 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
461 : {
462 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
463 0 : const ChainstateManager& chainman = EnsureChainman(node);
464 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
465 :
466 0 : uint256 protxHash(ParseHashV(request.params[0], "proTxHash"));
467 0 : int scanQuorumsCount = -1;
468 0 : if (!request.params[1].isNull()) {
469 0 : scanQuorumsCount = request.params[1].getInt<int>();
470 0 : if (scanQuorumsCount <= 0) {
471 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid scanQuorumsCount parameter");
472 : }
473 0 : }
474 :
475 0 : const CBlockIndex* pindexTip = WITH_LOCK(cs_main, return chainman.ActiveChain().Tip());
476 0 : auto mnList = CHECK_NONFATAL(node.dmnman)->GetListForBlock(pindexTip);
477 0 : auto dmn = mnList.GetMN(protxHash);
478 0 : if (!dmn) {
479 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "masternode not found");
480 : }
481 :
482 0 : UniValue result(UniValue::VARR);
483 0 : for (const auto& type : llmq::GetEnabledQuorumTypes(chainman, pindexTip)) {
484 0 : const auto llmq_params_opt = Params().GetLLMQ(type);
485 0 : CHECK_NONFATAL(llmq_params_opt.has_value());
486 0 : size_t count = llmq_params_opt->signingActiveQuorumCount;
487 0 : if (scanQuorumsCount != -1) {
488 0 : count = (size_t)scanQuorumsCount;
489 0 : }
490 0 : auto quorums = llmq_ctx.qman->ScanQuorums(llmq_params_opt->type, count);
491 0 : for (auto& quorum : quorums) {
492 0 : if (quorum->IsMember(dmn->proTxHash)) {
493 0 : auto json = BuildQuorumInfo(*llmq_ctx.quorum_block_processor, *quorum, false, false);
494 0 : json.pushKV("isValidMember", quorum->IsValidMember(dmn->proTxHash));
495 0 : json.pushKV("memberIndex", quorum->GetMemberIndex(dmn->proTxHash));
496 0 : result.push_back(json);
497 0 : }
498 : }
499 0 : }
500 :
501 0 : return result;
502 0 : },
503 : };
504 0 : }
505 :
506 0 : static UniValue quorum_sign_helper(const JSONRPCRequest& request, Consensus::LLMQType llmqType)
507 : {
508 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
509 0 : if (!node.active_ctx) {
510 0 : throw JSONRPCError(RPC_INTERNAL_ERROR, "Only available in masternode mode.");
511 : }
512 :
513 0 : const ChainstateManager& chainman = EnsureChainman(node);
514 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
515 :
516 0 : const auto llmq_params_opt = Params().GetLLMQ(llmqType);
517 0 : if (!llmq_params_opt.has_value()) {
518 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid LLMQ type");
519 : }
520 :
521 0 : const uint256 id(ParseHashV(request.params[0], "id"));
522 0 : const uint256 msgHash(ParseHashV(request.params[1], "msgHash"));
523 :
524 0 : uint256 quorumHash;
525 0 : if (!request.params[2].isNull() && !request.params[2].get_str().empty()) {
526 0 : quorumHash = ParseHashV(request.params[2], "quorumHash");
527 0 : }
528 0 : bool fSubmit{true};
529 0 : if (!request.params[3].isNull()) {
530 0 : fSubmit = ParseBoolV(request.params[3], "submit");
531 0 : }
532 0 : if (fSubmit) {
533 0 : return CHECK_NONFATAL(node.active_ctx)->shareman->AsyncSignIfMember(llmqType, *llmq_ctx.sigman, id, msgHash, quorumHash);
534 : } else {
535 0 : const auto pQuorum = [&]() {
536 0 : if (quorumHash.IsNull()) {
537 0 : return llmq::SelectQuorumForSigning(llmq_params_opt.value(), chainman.ActiveChain(), *llmq_ctx.qman, id);
538 : } else {
539 0 : return llmq_ctx.qman->GetQuorum(llmqType, quorumHash);
540 : }
541 0 : }();
542 :
543 0 : if (pQuorum == nullptr) {
544 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "quorum not found");
545 : }
546 :
547 0 : auto sigShare = CHECK_NONFATAL(node.active_ctx)->shareman->CreateSigShare(*pQuorum, id, msgHash);
548 :
549 0 : if (!sigShare.has_value() || !sigShare->sigShare.Get().IsValid()) {
550 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "failed to create sigShare");
551 : }
552 :
553 0 : UniValue obj(UniValue::VOBJ);
554 0 : obj.pushKV("llmqType", static_cast<uint8_t>(llmqType));
555 0 : obj.pushKV("quorumHash", sigShare->getQuorumHash().ToString());
556 0 : obj.pushKV("quorumMember", sigShare->getQuorumMember());
557 0 : obj.pushKV("id", id.ToString());
558 0 : obj.pushKV("msgHash", msgHash.ToString());
559 0 : obj.pushKV("signHash", sigShare->GetSignHash().ToString());
560 0 : obj.pushKV("signature", sigShare->sigShare.Get().ToString());
561 :
562 0 : return obj;
563 0 : }
564 0 : }
565 :
566 : namespace {
567 438 : const RPCResults quorum_sign_result{
568 146 : RPCResult{"if submit is set to true", RPCResult::Type::BOOL, "result", "result of signing, true if success"},
569 292 : RPCResult{"if submit is not set or set to false", RPCResult::Type::OBJ, "", "",
570 1168 : {
571 146 : {RPCResult::Type::NUM, "llmqType", "Quorum type"},
572 146 : {RPCResult::Type::STR_HEX, "quorumHash", "Quorum Hash"},
573 146 : {RPCResult::Type::NUM, "quorumMember", "Number of quorum member"},
574 146 : {RPCResult::Type::STR_HEX, "id", "Request ID"},
575 146 : {RPCResult::Type::STR_HEX, "msgHash", "Hash of message"},
576 146 : {RPCResult::Type::STR_HEX, "signHash", "Hash of signature"},
577 146 : {RPCResult::Type::STR_HEX, "signature", "Hex encoded signature"},
578 : },
579 : },
580 : };
581 : } // anonymous namespace
582 :
583 92 : static RPCHelpMan quorum_sign()
584 : {
585 184 : return RPCHelpMan{"quorum sign",
586 92 : "Threshold-sign a message\n",
587 552 : {
588 92 : {"llmqType", RPCArg::Type::NUM, RPCArg::Optional::NO, "LLMQ type."},
589 92 : {"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
590 92 : {"msgHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Message hash."},
591 92 : {"quorumHash", RPCArg::Type::STR_HEX, RPCArg::Default{""}, "The quorum identifier."},
592 92 : {"submit", RPCArg::Type::BOOL, RPCArg::Default{true}, "Submits the signature share to the network if this is true. "
593 : "Returns an object containing the signature share if this is false."},
594 : },
595 92 : quorum_sign_result,
596 92 : RPCExamples{""},
597 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
598 : {
599 0 : const Consensus::LLMQType llmqType{static_cast<Consensus::LLMQType>(request.params[0].getInt<int>())};
600 :
601 0 : JSONRPCRequest new_request{request};
602 0 : new_request.params.setArray();
603 0 : for (unsigned int i = 1; i < request.params.size(); ++i) {
604 0 : new_request.params.push_back(request.params[i]);
605 0 : }
606 0 : return quorum_sign_helper(new_request, llmqType);
607 0 : },
608 : };
609 0 : }
610 :
611 92 : static RPCHelpMan quorum_platformsign()
612 : {
613 184 : return RPCHelpMan{"quorum platformsign",
614 92 : "Threshold-sign a message. It signs messages only for platform quorums\n",
615 460 : {
616 92 : {"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
617 92 : {"msgHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Message hash."},
618 92 : {"quorumHash", RPCArg::Type::STR_HEX, RPCArg::Default{""}, "The quorum identifier."},
619 92 : {"submit", RPCArg::Type::BOOL, RPCArg::Default{true}, "Submits the signature share to the network if this is true. "
620 : "Returns an object containing the signature share if this is false."},
621 : },
622 92 : quorum_sign_result,
623 92 : RPCExamples{""},
624 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
625 : {
626 0 : const Consensus::LLMQType llmqType{Params().GetConsensus().llmqTypePlatform};
627 0 : return quorum_sign_helper(request, llmqType);
628 : },
629 : };
630 0 : }
631 :
632 0 : static bool VerifyRecoveredSigLatestQuorums(const Consensus::LLMQParams& llmq_params, const CChain& active_chain, const llmq::CQuorumManager& qman,
633 : int signHeight, const uint256& id, const uint256& msgHash, const CBLSSignature& sig)
634 : {
635 : // First check against the current active set, if it fails check against the last active set
636 0 : for (int signOffset : {0, llmq_params.dkgInterval}) {
637 0 : if (llmq::VerifyRecoveredSig(llmq_params.type, active_chain, qman, signHeight, id, msgHash, sig, signOffset) == llmq::VerifyRecSigStatus::Valid) {
638 0 : return true;
639 : }
640 : }
641 0 : return false;
642 0 : }
643 :
644 92 : static RPCHelpMan quorum_verify()
645 : {
646 184 : return RPCHelpMan{"quorum verify",
647 92 : "Test if a quorum signature is valid for a request id and a message hash\n",
648 644 : {
649 92 : {"llmqType", RPCArg::Type::NUM, RPCArg::Optional::NO, "LLMQ type."},
650 92 : {"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
651 92 : {"msgHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Message hash."},
652 92 : {"signature", RPCArg::Type::STR, RPCArg::Optional::NO, "Quorum signature to verify."},
653 92 : {"quorumHash", RPCArg::Type::STR_HEX, RPCArg::Default{""},
654 92 : "The quorum identifier.\n"
655 : "Set to \"\" if you want to specify signHeight instead."},
656 92 : {"signHeight", RPCArg::Type::NUM, RPCArg::Default{-1},
657 92 : "The height at which the message was signed.\n"
658 : "Only works when quorumHash is \"\"."},
659 : },
660 92 : RPCResult{RPCResult::Type::BOOL, "", "Returns true if the signature is valid"},
661 92 : RPCExamples{""},
662 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
663 : {
664 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
665 0 : const ChainstateManager& chainman = EnsureChainman(node);
666 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
667 :
668 0 : const Consensus::LLMQType llmqType{static_cast<Consensus::LLMQType>(request.params[0].getInt<int>())};
669 :
670 0 : const auto llmq_params_opt = Params().GetLLMQ(llmqType);
671 0 : if (!llmq_params_opt.has_value()) {
672 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid LLMQ type");
673 : }
674 :
675 0 : const uint256 id(ParseHashV(request.params[1], "id"));
676 0 : const uint256 msgHash(ParseHashV(request.params[2], "msgHash"));
677 :
678 0 : const bool use_bls_legacy = bls::bls_legacy_scheme.load();
679 0 : CBLSSignature sig;
680 0 : if (!sig.SetHexStr(request.params[3].get_str(), use_bls_legacy)) {
681 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid signature format");
682 : }
683 :
684 0 : if (request.params[4].isNull() || (request.params[4].get_str().empty() && !request.params[5].isNull())) {
685 0 : int signHeight{-1};
686 0 : if (!request.params[5].isNull()) {
687 0 : signHeight = request.params[5].getInt<int>();
688 0 : }
689 0 : return VerifyRecoveredSigLatestQuorums(*llmq_params_opt, chainman.ActiveChain(), *llmq_ctx.qman, signHeight, id, msgHash, sig);
690 : }
691 :
692 0 : uint256 quorumHash(ParseHashV(request.params[4], "quorumHash"));
693 0 : const auto quorum = llmq_ctx.qman->GetQuorum(llmqType, quorumHash);
694 :
695 0 : if (!quorum) {
696 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "quorum not found");
697 : }
698 :
699 0 : llmq::SignHash signHash{llmqType, quorum->qc->quorumHash, id, msgHash};
700 0 : return sig.VerifyInsecure(quorum->qc->quorumPublicKey, signHash.Get());
701 0 : },
702 : };
703 0 : }
704 :
705 92 : static RPCHelpMan quorum_hasrecsig()
706 : {
707 184 : return RPCHelpMan{"quorum hasrecsig",
708 92 : "Test if a valid recovered signature is present\n",
709 368 : {
710 92 : {"llmqType", RPCArg::Type::NUM, RPCArg::Optional::NO, "LLMQ type."},
711 92 : {"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
712 92 : {"msgHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Message hash."},
713 : },
714 92 : RPCResult{RPCResult::Type::BOOL, "", "Returns true if node has this recovered signature"},
715 92 : RPCExamples{""},
716 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
717 : {
718 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
719 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
720 :
721 0 : const Consensus::LLMQType llmqType{static_cast<Consensus::LLMQType>(request.params[0].getInt<int>())};
722 0 : if (!Params().GetLLMQ(llmqType).has_value()) {
723 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid LLMQ type");
724 : }
725 :
726 0 : const uint256 id(ParseHashV(request.params[1], "id"));
727 0 : const uint256 msgHash(ParseHashV(request.params[2], "msgHash"));
728 :
729 0 : return llmq_ctx.sigman->HasRecoveredSig(llmqType, id, msgHash);
730 0 : },
731 : };
732 0 : }
733 :
734 92 : static RPCHelpMan quorum_getrecsig()
735 : {
736 184 : return RPCHelpMan{"quorum getrecsig",
737 92 : "Get a recovered signature\n",
738 368 : {
739 92 : {"llmqType", RPCArg::Type::NUM, RPCArg::Optional::NO, "LLMQ type."},
740 92 : {"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
741 92 : {"msgHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Message hash."},
742 : },
743 92 : llmq::CRecoveredSig::GetJsonHelp(/*key=*/"", /*optional=*/false),
744 92 : RPCExamples{""},
745 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
746 : {
747 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
748 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
749 :
750 0 : const Consensus::LLMQType llmqType{static_cast<Consensus::LLMQType>(request.params[0].getInt<int>())};
751 0 : if (!Params().GetLLMQ(llmqType).has_value()) {
752 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid LLMQ type");
753 : }
754 :
755 0 : const uint256 id(ParseHashV(request.params[1], "id"));
756 0 : const uint256 msgHash(ParseHashV(request.params[2], "msgHash"));
757 :
758 0 : llmq::CRecoveredSig recSig;
759 0 : if (!llmq_ctx.sigman->GetRecoveredSigForId(llmqType, id, recSig)) {
760 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "recovered signature not found");
761 : }
762 0 : if (recSig.getMsgHash() != msgHash) {
763 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "recovered signature not found");
764 : }
765 0 : return recSig.ToJson();
766 0 : },
767 : };
768 0 : }
769 :
770 92 : static RPCHelpMan quorum_isconflicting()
771 : {
772 184 : return RPCHelpMan{"quorum isconflicting",
773 92 : "Test if a conflict exists\n",
774 368 : {
775 92 : {"llmqType", RPCArg::Type::NUM, RPCArg::Optional::NO, "LLMQ type."},
776 92 : {"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
777 92 : {"msgHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Message hash."},
778 : },
779 92 : RPCResult{RPCResult::Type::BOOL, "", "Returns true if this msgHash is conflicting with previous signing sessions"},
780 92 : RPCExamples{""},
781 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
782 : {
783 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
784 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
785 :
786 0 : const Consensus::LLMQType llmqType{static_cast<Consensus::LLMQType>(request.params[0].getInt<int>())};
787 0 : if (!Params().GetLLMQ(llmqType).has_value()) {
788 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid LLMQ type");
789 : }
790 :
791 0 : const uint256 id(ParseHashV(request.params[1], "id"));
792 0 : const uint256 msgHash(ParseHashV(request.params[2], "msgHash"));
793 :
794 0 : return llmq_ctx.sigman->IsConflicting(llmqType, id, msgHash);
795 0 : },
796 : };
797 0 : }
798 :
799 92 : static RPCHelpMan quorum_selectquorum()
800 : {
801 184 : return RPCHelpMan{"quorum selectquorum",
802 92 : "Returns the quorum that would/should sign a request\n",
803 276 : {
804 92 : {"llmqType", RPCArg::Type::NUM, RPCArg::Optional::NO, "LLMQ type."},
805 92 : {"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
806 : },
807 92 : RPCResult{
808 92 : RPCResult::Type::OBJ, "", "",
809 276 : {
810 92 : {RPCResult::Type::STR_HEX, "quorumHash", "Hash of chosen quorum"},
811 184 : {RPCResult::Type::ARR, "recoveryMembers", "List of members to use for signature recovery",
812 92 : {{RPCResult::Type::STR_HEX, "hash", "ProTxHash of member"}}
813 : },
814 : }
815 : },
816 92 : RPCExamples{""},
817 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
818 : {
819 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
820 0 : const ChainstateManager& chainman = EnsureChainman(node);
821 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
822 :
823 0 : const Consensus::LLMQType llmqType{static_cast<Consensus::LLMQType>(request.params[0].getInt<int>())};
824 0 : const auto llmq_params_opt = Params().GetLLMQ(llmqType);
825 0 : if (!llmq_params_opt.has_value()) {
826 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid LLMQ type");
827 : }
828 :
829 0 : const uint256 id(ParseHashV(request.params[1], "id"));
830 :
831 0 : UniValue ret(UniValue::VOBJ);
832 :
833 0 : const auto quorum = llmq::SelectQuorumForSigning(llmq_params_opt.value(), chainman.ActiveChain(), *llmq_ctx.qman, id);
834 0 : if (!quorum) {
835 0 : throw JSONRPCError(RPC_MISC_ERROR, "no quorums active");
836 : }
837 0 : ret.pushKV("quorumHash", quorum->qc->quorumHash.ToString());
838 :
839 0 : UniValue recoveryMembers(UniValue::VARR);
840 0 : for (int i = 0; i < quorum->params.recoveryMembers; ++i) {
841 0 : auto dmn = llmq::CSigSharesManager::SelectMemberForRecovery(*quorum, id, i);
842 0 : recoveryMembers.push_back(dmn->proTxHash.ToString());
843 0 : }
844 0 : ret.pushKV("recoveryMembers", recoveryMembers);
845 :
846 0 : return ret;
847 0 : },
848 : };
849 0 : }
850 :
851 92 : static RPCHelpMan quorum_dkgsimerror()
852 : {
853 184 : return RPCHelpMan{"quorum dkgsimerror",
854 92 : "This enables simulation of errors and malicious behaviour in the DKG. Do NOT use this on mainnet\n"
855 : "as you will get yourself very likely PoSe banned for this.\n",
856 276 : {
857 92 : {"type", RPCArg::Type::STR, RPCArg::Optional::NO, "Error type."},
858 92 : {"rate", RPCArg::Type::NUM, RPCArg::Optional::NO, "Rate at which to simulate this error type (between 0 and 100)."},
859 : },
860 92 : RPCResult{RPCResult::Type::NONE, "", ""},
861 92 : RPCExamples{""},
862 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
863 : {
864 0 : std::string type_str = request.params[0].get_str();
865 0 : int32_t rate = request.params[1].getInt<int>();
866 :
867 0 : if (rate < 0 || rate > 100) {
868 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid rate. Must be between 0 and 100");
869 : }
870 :
871 0 : if (const llmq::DKGError::type type = llmq::DKGError::from_string(type_str);
872 0 : type == llmq::DKGError::type::_COUNT) {
873 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid type. See DKGError class implementation");
874 : } else {
875 0 : llmq::SetSimulatedDKGErrorRate(type, static_cast<double>(rate) / 100);
876 0 : return NullUniValue;
877 : }
878 0 : },
879 : };
880 0 : }
881 :
882 92 : static RPCHelpMan quorum_getdata()
883 : {
884 184 : return RPCHelpMan{"quorum getdata",
885 92 : "Send a QGETDATA message to the specified peer.\n",
886 552 : {
887 92 : {"nodeId", RPCArg::Type::NUM, RPCArg::Optional::NO, "The internal nodeId of the peer to request quorum data from."},
888 92 : {"llmqType", RPCArg::Type::NUM, RPCArg::Optional::NO, "The quorum type related to the quorum data being requested."},
889 92 : {"quorumHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The quorum hash related to the quorum data being requested."},
890 92 : {"dataMask", RPCArg::Type::NUM, RPCArg::Optional::NO,
891 92 : "Specify what data to request.\n"
892 : "Possible values: 1 - Request quorum verification vector\n"
893 : "2 - Request encrypted contributions for member defined by \"proTxHash\". \"proTxHash\" must be specified if this option is used.\n"
894 : "3 - Request both, 1 and 2"},
895 92 : {"proTxHash", RPCArg::Type::STR_HEX, RPCArg::Default{""}, "The proTxHash the contributions will be requested for. Must be member of the specified LLMQ."},
896 : },
897 92 : RPCResult{RPCResult::Type::BOOL, "", "Returns true if the message QGETDATA has been successfully sent"},
898 92 : RPCExamples{""},
899 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
900 : {
901 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
902 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
903 0 : CConnman& connman = EnsureConnman(node);
904 :
905 0 : NodeId nodeId = request.params[0].getInt<int64_t>();
906 0 : Consensus::LLMQType llmqType = static_cast<Consensus::LLMQType>(request.params[1].getInt<int>());
907 0 : uint256 quorumHash(ParseHashV(request.params[2], "quorumHash"));
908 0 : uint16_t nDataMask = static_cast<uint16_t>(request.params[3].getInt<int>());
909 0 : uint256 proTxHash;
910 :
911 : // Check if request wants ENCRYPTED_CONTRIBUTIONS data
912 0 : if (nDataMask & llmq::CQuorumDataRequest::ENCRYPTED_CONTRIBUTIONS) {
913 0 : if (!request.params[4].isNull()) {
914 0 : proTxHash = ParseHashV(request.params[4], "proTxHash");
915 0 : if (proTxHash.IsNull()) {
916 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "proTxHash invalid");
917 : }
918 0 : } else {
919 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "proTxHash missing");
920 : }
921 0 : }
922 :
923 0 : const auto quorum = llmq_ctx.qman->GetQuorum(llmqType, quorumHash);
924 0 : if (!quorum) {
925 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "quorum not found");
926 : }
927 0 : return connman.ForNode(nodeId, [&](CNode* pNode) {
928 0 : if (pNode->GetVerifiedProRegTxHash().IsNull()) return false;
929 0 : if (!quorum->m_quorum_base_block_index) return false;
930 0 : const llmq::CQuorumDataRequest request(llmqType, quorum->qc->quorumHash, nDataMask, proTxHash);
931 0 : const llmq::CQuorumDataRequestKey key(pNode->GetVerifiedProRegTxHash(), true, quorum->qc->quorumHash, llmqType);
932 0 : if (!llmq_ctx.qman->RegisterDataRequest(key, request)) return false;
933 0 : connman.PushMessage(pNode, CNetMsgMaker(pNode->GetCommonVersion()).Make(NetMsgType::QGETDATA, request));
934 0 : return true;
935 0 : });
936 0 : },
937 : };
938 0 : }
939 :
940 92 : static RPCHelpMan quorum_rotationinfo()
941 : {
942 92 : return RPCHelpMan{
943 92 : "quorum rotationinfo",
944 92 : "Get quorum rotation information\n",
945 368 : {
946 92 : {"blockRequestHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The blockHash of the request."},
947 92 : {"extraShare", RPCArg::Type::BOOL, RPCArg::Default{false}, "Extra share"},
948 184 : {"baseBlockHashes", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "The list of block hashes",
949 184 : {
950 92 : {"baseBlockHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The block hash"},
951 : }},
952 : },
953 92 : llmq::CQuorumRotationInfo::GetJsonHelp(/*key=*/"", /*optional=*/false),
954 92 : RPCExamples{""},
955 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
956 : {
957 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
958 0 : const ChainstateManager& chainman = EnsureChainman(node);
959 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
960 :
961 0 : llmq::CGetQuorumRotationInfo cmd;
962 0 : llmq::CQuorumRotationInfo quorumRotationInfoRet;
963 0 : std::string strError;
964 :
965 0 : cmd.blockRequestHash = ParseHashV(request.params[0], "blockRequestHash");
966 0 : cmd.extraShare = request.params[1].isNull() ? false : ParseBoolV(request.params[1], "extraShare");
967 :
968 0 : if (!request.params[2].isNull()) {
969 0 : const auto& hashes = request.params[2].get_array();
970 0 : for (const auto& hash : hashes.getValues()) {
971 0 : cmd.baseBlockHashes.emplace_back(ParseHashV(hash, "baseBlockHash"));
972 : }
973 0 : }
974 :
975 0 : LOCK(cs_main);
976 :
977 0 : if (!BuildQuorumRotationInfo(*CHECK_NONFATAL(node.dmnman), *llmq_ctx.qsnapman, chainman, *llmq_ctx.qman,
978 0 : *llmq_ctx.quorum_block_processor, cmd, false, quorumRotationInfoRet, strError)) {
979 0 : throw JSONRPCError(RPC_INVALID_REQUEST, strError);
980 : }
981 :
982 0 : return quorumRotationInfoRet.ToJson();
983 0 : },
984 : };
985 0 : }
986 :
987 92 : static RPCHelpMan quorum_dkginfo()
988 : {
989 92 : return RPCHelpMan{
990 92 : "quorum dkginfo",
991 92 : "Return information regarding DKGs.\n",
992 92 : {
993 92 : {},
994 : },
995 92 : RPCResult{
996 92 : RPCResult::Type::OBJ, "", "",
997 276 : {
998 92 : {RPCResult::Type::NUM, "active_dkgs", "Total number of active DKG sessions this node is participating in right now"},
999 92 : {RPCResult::Type::NUM, "next_dkg", "The number of blocks until the next potential DKG session"},
1000 : }
1001 : },
1002 92 : RPCExamples{""},
1003 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
1004 : {
1005 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
1006 0 : if (!node.active_ctx && !node.observer_ctx) {
1007 0 : throw JSONRPCError(RPC_INTERNAL_ERROR, "Only available in masternode or watch-only mode.");
1008 : }
1009 0 : const auto& dkgdbgman = *(node.active_ctx ? node.active_ctx->dkgdbgman.get() : node.observer_ctx->dkgdbgman.get());
1010 :
1011 0 : UniValue ret(UniValue::VOBJ);
1012 0 : ret.pushKV("active_dkgs", dkgdbgman.GetSessionCount());
1013 :
1014 0 : const ChainstateManager& chainman = EnsureChainman(node);
1015 0 : const int nTipHeight{WITH_LOCK(cs_main, return chainman.ActiveChain().Height())};
1016 0 : auto minNextDKG = [](const Consensus::Params& consensusParams, int nTipHeight) {
1017 0 : int minDkgWindow{std::numeric_limits<int>::max()};
1018 0 : for (const auto& params: consensusParams.llmqs) {
1019 0 : if (params.useRotation && (nTipHeight % params.dkgInterval <= params.signingActiveQuorumCount)) {
1020 0 : return 1;
1021 : }
1022 0 : minDkgWindow = std::min(minDkgWindow, params.dkgInterval - (nTipHeight % params.dkgInterval));
1023 : }
1024 0 : return minDkgWindow;
1025 0 : };
1026 0 : ret.pushKV("next_dkg", minNextDKG(Params().GetConsensus(), nTipHeight));
1027 :
1028 0 : return ret;
1029 0 : },
1030 : };
1031 0 : }
1032 :
1033 92 : static RPCHelpMan quorum_help()
1034 : {
1035 92 : return RPCHelpMan{
1036 92 : "quorum",
1037 92 : "Set of commands for quorums/LLMQs.\n"
1038 : "To get help on individual commands, use \"help quorum command\".\n"
1039 : "\nAvailable commands:\n"
1040 : " list - List of on-chain quorums\n"
1041 : " listextended - Extended list of on-chain quorums\n"
1042 : " info - Return information about a quorum\n"
1043 : " dkginfo - Return information about DKGs\n"
1044 : " dkgsimerror - Simulates DKG errors and malicious behavior\n"
1045 : " dkgstatus - Return the status of the current DKG process\n"
1046 : " memberof - Checks which quorums the given masternode is a member of\n"
1047 : " sign - Threshold-sign a message\n"
1048 : " verify - Test if a quorum signature is valid for a request id and a message hash\n"
1049 : " hasrecsig - Test if a valid recovered signature is present\n"
1050 : " getrecsig - Get a recovered signature\n"
1051 : " isconflicting - Test if a conflict exists\n"
1052 : " selectquorum - Return the quorum that would/should sign a request\n"
1053 : " getdata - Request quorum data from other masternodes in the quorum\n"
1054 : " rotationinfo - Request quorum rotation information\n",
1055 184 : {
1056 92 : {"command", RPCArg::Type::STR, RPCArg::Optional::NO, "The command to execute"},
1057 : },
1058 92 : RPCResult{RPCResult::Type::NONE, "", ""},
1059 92 : RPCExamples{""},
1060 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
1061 : {
1062 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Must be a valid command");
1063 0 : },
1064 : };
1065 0 : }
1066 :
1067 92 : static RPCHelpMan verifychainlock()
1068 : {
1069 184 : return RPCHelpMan{"verifychainlock",
1070 92 : "Test if a quorum signature is valid for a ChainLock.\n",
1071 368 : {
1072 92 : {"blockHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The block hash of the ChainLock."},
1073 92 : {"signature", RPCArg::Type::STR, RPCArg::Optional::NO, "The signature of the ChainLock."},
1074 92 : {"blockHeight", RPCArg::Type::NUM, RPCArg::DefaultHint{"There will be an internal lookup of \"blockHash\" if this is not provided."}, "The height of the ChainLock."},
1075 : },
1076 92 : RPCResult{RPCResult::Type::BOOL, "", "Returns true if the chainlock is valid"},
1077 92 : RPCExamples{""},
1078 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
1079 : {
1080 0 : const uint256 nBlockHash(ParseHashV(request.params[0], "blockHash"));
1081 :
1082 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
1083 0 : const ChainstateManager& chainman = EnsureChainman(node);
1084 :
1085 : int nBlockHeight;
1086 0 : const CBlockIndex* pIndex{nullptr};
1087 0 : if (request.params[2].isNull()) {
1088 0 : pIndex = WITH_LOCK(cs_main, return chainman.m_blockman.LookupBlockIndex(nBlockHash));
1089 0 : if (pIndex == nullptr) {
1090 0 : throw JSONRPCError(RPC_INTERNAL_ERROR, "blockHash not found");
1091 : }
1092 0 : nBlockHeight = pIndex->nHeight;
1093 0 : } else {
1094 0 : nBlockHeight = request.params[2].getInt<int>();
1095 0 : LOCK(cs_main);
1096 0 : if (nBlockHeight < 0) {
1097 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range");
1098 : }
1099 0 : if (nBlockHeight <= chainman.ActiveChain().Height()) {
1100 0 : pIndex = chainman.ActiveChain()[nBlockHeight];
1101 0 : }
1102 0 : }
1103 :
1104 0 : CBLSSignature sig;
1105 0 : if (pIndex) {
1106 0 : const bool use_legacy_signature{!DeploymentActiveAfter(pIndex, chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)};
1107 0 : if (!sig.SetHexStr(request.params[1].get_str(), use_legacy_signature)) {
1108 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid signature format");
1109 : }
1110 0 : } else {
1111 0 : if (!sig.SetHexStr(request.params[1].get_str(), false) &&
1112 0 : !sig.SetHexStr(request.params[1].get_str(), true)
1113 : ) {
1114 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid signature format");
1115 : }
1116 : }
1117 :
1118 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
1119 0 : return chainlock::VerifyChainLock(Params().GetConsensus(), chainman.ActiveChain(), *CHECK_NONFATAL(llmq_ctx.qman),
1120 0 : chainlock::ChainLockSig{nBlockHeight, nBlockHash, sig}) ==
1121 : llmq::VerifyRecSigStatus::Valid;
1122 0 : },
1123 : };
1124 0 : }
1125 :
1126 92 : static RPCHelpMan verifyislock()
1127 : {
1128 184 : return RPCHelpMan{"verifyislock",
1129 92 : "Test if a quorum signature is valid for an InstantSend Lock\n",
1130 460 : {
1131 92 : {"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
1132 92 : {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id."},
1133 92 : {"signature", RPCArg::Type::STR, RPCArg::Optional::NO, "The InstantSend Lock signature to verify."},
1134 92 : {"maxHeight", RPCArg::Type::NUM, RPCArg::Default{-1}, "The maximum height to search quorums from."},
1135 : },
1136 92 : RPCResult{RPCResult::Type::BOOL, "", "Returns true if the instantsend lock is valid"},
1137 92 : RPCExamples{""},
1138 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
1139 : {
1140 0 : const uint256 id(ParseHashV(request.params[0], "id"));
1141 0 : const uint256 txid(ParseHashV(request.params[1], "txid"));
1142 :
1143 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
1144 0 : const ChainstateManager& chainman = EnsureChainman(node);
1145 :
1146 0 : if (g_txindex) {
1147 0 : g_txindex->BlockUntilSyncedToCurrentChain();
1148 0 : }
1149 :
1150 0 : const CBlockIndex* pindexMined{nullptr};
1151 : {
1152 0 : LOCK(cs_main);
1153 0 : uint256 hash_block;
1154 0 : CTransactionRef tx = GetTransaction(/* block_index */ nullptr, /* mempool */ nullptr, txid, Params().GetConsensus(), hash_block);
1155 0 : if (tx && !hash_block.IsNull()) {
1156 0 : pindexMined = chainman.m_blockman.LookupBlockIndex(hash_block);
1157 0 : }
1158 0 : }
1159 :
1160 0 : int maxHeight{-1};
1161 0 : if (!request.params[3].isNull()) {
1162 0 : maxHeight = request.params[3].getInt<int>();
1163 0 : }
1164 :
1165 : int signHeight;
1166 0 : if (pindexMined == nullptr || pindexMined->nHeight > maxHeight) {
1167 0 : signHeight = maxHeight;
1168 0 : } else { // pindexMined->nHeight <= maxHeight
1169 0 : signHeight = pindexMined->nHeight;
1170 : }
1171 :
1172 0 : CBlockIndex* pBlockIndex{nullptr};
1173 : {
1174 0 : LOCK(cs_main);
1175 0 : if (signHeight == -1) {
1176 0 : pBlockIndex = chainman.ActiveChain().Tip();
1177 0 : } else {
1178 0 : pBlockIndex = chainman.ActiveChain()[signHeight];
1179 : }
1180 0 : }
1181 :
1182 0 : CHECK_NONFATAL(pBlockIndex != nullptr);
1183 :
1184 0 : CBLSSignature sig;
1185 0 : const bool use_bls_legacy{!DeploymentActiveAfter(pBlockIndex, chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)};
1186 0 : if (!sig.SetHexStr(request.params[2].get_str(), use_bls_legacy)) {
1187 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid signature format");
1188 : }
1189 :
1190 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
1191 :
1192 0 : auto llmqType = Params().GetConsensus().llmqTypeDIP0024InstantSend;
1193 0 : const auto llmq_params_opt = Params().GetLLMQ(llmqType);
1194 0 : CHECK_NONFATAL(llmq_params_opt.has_value());
1195 0 : return VerifyRecoveredSigLatestQuorums(*llmq_params_opt, chainman.ActiveChain(), *CHECK_NONFATAL(llmq_ctx.qman),
1196 0 : signHeight, id, txid, sig);
1197 0 : },
1198 : };
1199 0 : }
1200 :
1201 92 : static RPCHelpMan submitchainlock()
1202 : {
1203 184 : return RPCHelpMan{"submitchainlock",
1204 92 : "Submit a ChainLock signature if needed\n",
1205 368 : {
1206 92 : {"blockHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The block hash of the ChainLock."},
1207 92 : {"signature", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The signature of the ChainLock."},
1208 92 : {"blockHeight", RPCArg::Type::NUM, RPCArg::Optional::NO, "The height of the ChainLock."},
1209 : },
1210 92 : RPCResult{
1211 92 : RPCResult::Type::NUM, "", "The height of the current best ChainLock"},
1212 92 : RPCExamples{""},
1213 92 : [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
1214 : {
1215 0 : const uint256 nBlockHash(ParseHashV(request.params[0], "blockHash"));
1216 :
1217 0 : const int nBlockHeight = request.params[2].getInt<int>();
1218 0 : if (nBlockHeight <= 0) {
1219 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid block height");
1220 : }
1221 0 : const NodeContext& node = EnsureAnyNodeContext(request.context);
1222 0 : const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
1223 0 : CHECK_NONFATAL(node.chainlocks);
1224 0 : const int32_t bestCLHeight = node.chainlocks->GetBestChainLock().getHeight();
1225 0 : if (nBlockHeight <= bestCLHeight) return bestCLHeight;
1226 :
1227 0 : CBLSSignature sig;
1228 0 : if (!sig.SetHexStr(request.params[1].get_str(), false) && !sig.SetHexStr(request.params[1].get_str(), true)) {
1229 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid signature format");
1230 : }
1231 :
1232 0 : const ChainstateManager& chainman = EnsureChainman(node);
1233 0 : const auto clsig{chainlock::ChainLockSig(nBlockHeight, nBlockHash, sig)};
1234 0 : const llmq::VerifyRecSigStatus ret{
1235 0 : chainlock::VerifyChainLock(Params().GetConsensus(), chainman.ActiveChain(), *llmq_ctx.qman, clsig)};
1236 0 : if (ret == llmq::VerifyRecSigStatus::NoQuorum) {
1237 0 : LOCK(cs_main);
1238 0 : const CBlockIndex* pIndex{chainman.ActiveChain().Tip()};
1239 0 : throw JSONRPCError(RPC_MISC_ERROR, strprintf("No quorum found. Current tip height: %d hash: %s\n", pIndex->nHeight, pIndex->GetBlockHash().ToString()));
1240 0 : }
1241 0 : if (ret != llmq::VerifyRecSigStatus::Valid) {
1242 0 : throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid signature");
1243 : }
1244 :
1245 0 : PeerManager& peerman = EnsurePeerman(node);
1246 0 : CHECK_NONFATAL(node.clhandler);
1247 0 : peerman.PostProcessMessage(node.clhandler->ProcessNewChainLock(-1, clsig, *llmq_ctx.qman, ::SerializeHash(clsig)));
1248 0 : return node.chainlocks->GetBestChainLock().getHeight();
1249 0 : },
1250 : };
1251 0 : }
1252 :
1253 :
1254 178 : void RegisterQuorumsRPCCommands(CRPCTable &tableRPC)
1255 : {
1256 1098 : static const CRPCCommand commands[]{
1257 46 : {"evo", &quorum_help},
1258 46 : {"evo", &quorum_list},
1259 46 : {"evo", &quorum_list_extended},
1260 46 : {"evo", &quorum_info},
1261 46 : {"evo", &quorum_dkginfo},
1262 46 : {"evo", &quorum_dkgstatus},
1263 46 : {"evo", &quorum_memberof},
1264 46 : {"evo", &quorum_sign},
1265 46 : {"evo", &quorum_platformsign},
1266 46 : {"evo", &quorum_verify},
1267 46 : {"evo", &quorum_hasrecsig},
1268 46 : {"evo", &quorum_getrecsig},
1269 46 : {"evo", &quorum_isconflicting},
1270 46 : {"evo", &quorum_selectquorum},
1271 46 : {"evo", &quorum_dkgsimerror},
1272 46 : {"evo", &quorum_getdata},
1273 46 : {"evo", &quorum_rotationinfo},
1274 46 : {"evo", &submitchainlock},
1275 46 : {"evo", &verifychainlock},
1276 46 : {"evo", &verifyislock},
1277 : };
1278 3738 : for (const auto& command : commands) {
1279 3560 : tableRPC.appendCommand(command.name, &command);
1280 : }
1281 178 : }
|