From 3bc77a6e1d2740fcdfc6e7675d955272028ca9da Mon Sep 17 00:00:00 2001 From: Odysseas Gabrielides Date: Tue, 19 Dec 2023 06:27:19 +0200 Subject: [PATCH] feat(rpc): submit chainlock signature if needed RPC (#5765) ## Issue being fixed or feature implemented Once Platform is live, there could be an edge case where the CL could arrive to an EvoNode faster through Platform quorum than regular P2P propagation. ## What was done? This PR introduces a new RPC `submitchainlock` with the following 3 mandatory parameters: - `blockHash`, `signature` and `height`. Besides some basic tests: - If the block is unknown then the RPC returns an error (could happen if the node is stucked) - If the signature is not verified then the RPC return an error. - If the node already has this CL, the RPC returns true. - If the node doesn't have this CL, it inserts it, broadcast it through the inv system and return true. ## How Has This Been Tested? `feature_llmq_chainlocks.py` was modified with the following scenario: 1. node0 is isolated from the rest of the network 2. node1 mines a new block and waits for CL 3. Make sure node0 doesn't know the new block/CL (by checking `getbestchainlock()`) 4. CL is submitted via the new RPC on node0 5. checking `getbestchainlock()` and make sure the CL was processed + 'known_block' is false 6. reconnect node0 ## Breaking Changes no ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ --------- Co-authored-by: UdjinM6 Co-authored-by: thephez --- doc/release-notes-5765.md | 5 +++ src/rpc/quorums.cpp | 44 ++++++++++++++++++++++ src/rpc/server.cpp | 1 + test/functional/feature_llmq_chainlocks.py | 19 ++++++++++ test/functional/rpc_platform_filter.py | 1 + 5 files changed, 70 insertions(+) create mode 100644 doc/release-notes-5765.md diff --git a/doc/release-notes-5765.md b/doc/release-notes-5765.md new file mode 100644 index 0000000000000..c60271728ac09 --- /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 eb553fed2ac50..a5ad1965e55df 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 6ccd7c278fc2f..7f11e4b70b50e 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 37dc976eee4f0..6209e8bb7a021 100755 --- a/test/functional/feature_llmq_chainlocks.py +++ b/test/functional/feature_llmq_chainlocks.py @@ -98,6 +98,25 @@ def run_test(self): 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 5dc0437fffe01..383c83fa1921f 100755 --- a/test/functional/rpc_platform_filter.py +++ b/test/functional/rpc_platform_filter.py @@ -61,6 +61,7 @@ def test_command(method, params, auth, expexted_status, should_not_match=False): "getblockcount", "getbestchainlock", "quorum", + "submitchainlock", "verifyislock"] help_output = self.nodes[0].help().split('\n')