diff --git a/doc/release-notes-5765.md b/doc/release-notes-5765.md new file mode 100644 index 0000000000..c60271728a --- /dev/null +++ b/doc/release-notes-5765.md @@ -0,0 +1,5 @@ +Added RPC +-------- + +- `submitchainlock` RPC allows the submission of a ChainLock signature. +Note: This RPC is whitelisted for the Platform RPC user. diff --git a/src/rpc/quorums.cpp b/src/rpc/quorums.cpp index eb553fed2a..a5ad1965e5 100644 --- a/src/rpc/quorums.cpp +++ b/src/rpc/quorums.cpp @@ -986,6 +986,49 @@ static UniValue verifyislock(const JSONRPCRequest& request) return llmq_ctx.sigman->VerifyRecoveredSig(llmqType, *llmq_ctx.qman, signHeight, id, txid, sig, 0) || llmq_ctx.sigman->VerifyRecoveredSig(llmqType, *llmq_ctx.qman, signHeight, id, txid, sig, signOffset); } + +static void submitchainlock_help(const JSONRPCRequest& request) +{ + RPCHelpMan{"submitchainlock", + "Submit a ChainLock signature if needed\n", + { + {"blockHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The block hash of the ChainLock."}, + {"signature", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The signature of the ChainLock."}, + {"blockHeight", RPCArg::Type::NUM, RPCArg::Optional::NO, "The height of the ChainLock."}, + }, + RPCResults{}, + RPCExamples{""}, + }.Check(request); +} + +static UniValue submitchainlock(const JSONRPCRequest& request) +{ + submitchainlock_help(request); + + const uint256 nBlockHash(ParseHashV(request.params[0], "blockHash")); + + const int nBlockHeight = ParseInt32V(request.params[2], "blockHeight"); + if (nBlockHeight <= 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid block height"); + } + + CBLSSignature sig; + if (!sig.SetHexStr(request.params[1].get_str(), false) && !sig.SetHexStr(request.params[1].get_str(), true)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid signature format"); + } + + + const LLMQContext& llmq_ctx = EnsureLLMQContext(EnsureAnyNodeContext(request.context)); + auto clsig = llmq::CChainLockSig(nBlockHeight, nBlockHash, sig); + if (!llmq_ctx.clhandler->VerifyChainLock(clsig)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid signature"); + } + + llmq_ctx.clhandler->ProcessNewChainLock(-1, clsig, ::SerializeHash(clsig)); + return true; +} + + void RegisterQuorumsRPCCommands(CRPCTable &tableRPC) { // clang-format off @@ -993,6 +1036,7 @@ static const CRPCCommand commands[] = { // category name actor (function) // --------------------- ------------------------ ----------------------- { "evo", "quorum", &_quorum, {} }, + { "evo", "submitchainlock", &submitchainlock, {"blockHash", "signature", "blockHeight"} }, { "evo", "verifychainlock", &verifychainlock, {"blockHash", "signature", "blockHeight"} }, { "evo", "verifyislock", &verifyislock, {"id", "txid", "signature", "maxHeight"} }, }; diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index 6ccd7c278f..7f11e4b70b 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -147,6 +147,7 @@ void CRPCTable::InitPlatformRestrictions() {"getbestchainlock", {}}, {"quorum", {"sign", static_cast(Params().GetConsensus().llmqTypePlatform)}}, {"quorum", {"verify"}}, + {"submitchainlock", {}}, {"verifyislock", {}}, }; } diff --git a/test/functional/feature_llmq_chainlocks.py b/test/functional/feature_llmq_chainlocks.py index 37dc976eee..6209e8bb7a 100755 --- a/test/functional/feature_llmq_chainlocks.py +++ b/test/functional/feature_llmq_chainlocks.py @@ -98,6 +98,25 @@ class LLMQChainLocksTest(DashTestFramework): self.wait_for_chainlocked_block_all_nodes(self.nodes[1].getbestblockhash()) self.test_coinbase_best_cl(self.nodes[0]) + self.log.info("Isolate node, mine on another, reconnect and submit CL via RPC") + self.isolate_node(0) + self.nodes[1].generate(1) + self.wait_for_chainlocked_block(self.nodes[1], self.nodes[1].getbestblockhash()) + best_0 = self.nodes[0].getbestchainlock() + best_1 = self.nodes[1].getbestchainlock() + assert best_0['blockhash'] != best_1['blockhash'] + assert best_0['height'] != best_1['height'] + assert best_0['signature'] != best_1['signature'] + assert_equal(best_0['known_block'], True) + self.nodes[0].submitchainlock(best_1['blockhash'], best_1['signature'], best_1['height']) + best_0 = self.nodes[0].getbestchainlock() + assert_equal(best_0['blockhash'], best_1['blockhash']) + assert_equal(best_0['height'], best_1['height']) + assert_equal(best_0['signature'], best_1['signature']) + assert_equal(best_0['known_block'], False) + self.reconnect_isolated_node(0, 1) + self.sync_all() + self.log.info("Isolate node, mine on both parts of the network, and reconnect") self.isolate_node(0) bad_tip = self.nodes[0].generate(5)[-1] diff --git a/test/functional/rpc_platform_filter.py b/test/functional/rpc_platform_filter.py index 5dc0437fff..383c83fa19 100755 --- a/test/functional/rpc_platform_filter.py +++ b/test/functional/rpc_platform_filter.py @@ -61,6 +61,7 @@ class HTTPBasicsTest(BitcoinTestFramework): "getblockcount", "getbestchainlock", "quorum", + "submitchainlock", "verifyislock"] help_output = self.nodes[0].help().split('\n')