diff --git a/doc/release-notes-6455.md b/doc/release-notes-6455.md new file mode 100644 index 0000000000000..4b20c00a701d8 --- /dev/null +++ b/doc/release-notes-6455.md @@ -0,0 +1,11 @@ +## New RPCs + +- **`getislocks`** + - Retrieves the InstantSend lock data for the given transaction IDs (txids). + Returns the lock information in both human-friendly JSON format and binary hex-encoded zmq-compatible format. + +## Updated RPCs + +- **`getbestchainlock` Changes** + - A new hex field has been added to the getbestchainlock RPC, which returns the ChainLock information in zmq-compatible, hex-encoded binary format. + diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 871ef727b15ab..8daeca5962be2 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -241,6 +241,7 @@ static RPCHelpMan getbestchainlock() {RPCResult::Type::NUM, "height", "The block height or index"}, {RPCResult::Type::STR_HEX, "signature", "The ChainLock's BLS signature"}, {RPCResult::Type::BOOL, "known_block", "True if the block is known by our node"}, + {RPCResult::Type::STR_HEX, "hex", "The serialized, hex-encoded data for best ChainLock"}, }}, RPCExamples{ HelpExampleCli("getbestchainlock", "") @@ -248,8 +249,6 @@ static RPCHelpMan getbestchainlock() }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { - UniValue result(UniValue::VOBJ); - const NodeContext& node = EnsureAnyNodeContext(request.context); const LLMQContext& llmq_ctx = EnsureLLMQContext(node); @@ -257,13 +256,23 @@ static RPCHelpMan getbestchainlock() if (clsig.IsNull()) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to find any ChainLock"); } + + UniValue result(UniValue::VOBJ); + result.pushKV("blockhash", clsig.getBlockHash().GetHex()); result.pushKV("height", clsig.getHeight()); result.pushKV("signature", clsig.getSig().ToString()); - const ChainstateManager& chainman = EnsureChainman(node); - LOCK(cs_main); - result.pushKV("known_block", chainman.m_blockman.LookupBlockIndex(clsig.getBlockHash()) != nullptr); + { + const ChainstateManager& chainman = EnsureChainman(node); + LOCK(cs_main); + result.pushKV("known_block", chainman.m_blockman.LookupBlockIndex(clsig.getBlockHash()) != nullptr); + } + + CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); + ssTx << clsig; + result.pushKV("hex", HexStr(ssTx)); + return result; }, }; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 4ad0c584076ea..658added3d683 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -115,6 +115,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "gettransaction", 1, "include_watchonly" }, { "gettransaction", 2, "verbose" }, { "getrawtransaction", 1, "verbose" }, + { "getislocks", 0, "txids" }, { "getrawtransactionmulti", 0, "transactions" }, { "getrawtransactionmulti", 1, "verbose" }, { "gettxchainlocks", 0, "txids" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 4586662fa1f64..da1dc54003444 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -300,6 +300,7 @@ static RPCHelpMan getrawtransactionmulti() { {"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, "If false, return a string, otherwise return a json object"}, }, + // TODO: replace RPCResults to proper annotation RPCResults{}, RPCExamples{ HelpExampleCli("getrawtransactionmulti", @@ -366,6 +367,86 @@ static RPCHelpMan getrawtransactionmulti() { }; } +static RPCHelpMan getislocks() +{ + return RPCHelpMan{"getislocks", + "\nReturns the raw InstantSend lock data for each txids. Returns Null if there is no known IS yet.", + { + {"txids", RPCArg::Type::ARR, RPCArg::Optional::NO, "The transaction ids (no more than 100)", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A transaction hash"}, + }, + }, + }, + RPCResult{ + RPCResult::Type::ARR, "", "Response is an array with the same size as the input txids", + {{RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "txid", "The transaction id"}, + {RPCResult::Type::ARR, "inputs", "The inputs", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "txid", "The transaction id"}, + {RPCResult::Type::NUM, "vout", "The output number"}, + }, + }, + }}, + {RPCResult::Type::STR_HEX, "cycleHash", "The Cycle Hash"}, + {RPCResult::Type::STR_HEX, "signature", "The InstantSend's BLS signature"}, + {RPCResult::Type::STR_HEX, "hex", "The serialized, hex-encoded data for 'txid'"}, + }}, + RPCResult{"if no InstantSend Lock is known for specified txid", + RPCResult::Type::STR, "data", "Just 'None' string" + }, + }}, + RPCExamples{ + HelpExampleCli("getislocks", "'[\"txid\",...]'") + + HelpExampleRpc("getislocks", "'[\"txid\",...]'") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const NodeContext& node = EnsureAnyNodeContext(request.context); + + UniValue result_arr(UniValue::VARR); + UniValue txids = request.params[0].get_array(); + if (txids.size() > 100) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Up to 100 txids only"); + } + + const LLMQContext& llmq_ctx = EnsureLLMQContext(node); + for (const auto idx : irange::range(txids.size())) { + const uint256 txid(ParseHashV(txids[idx], "txid")); + + if (const llmq::CInstantSendLockPtr islock = llmq_ctx.isman->GetInstantSendLockByTxid(txid); islock != nullptr) { + UniValue objIS(UniValue::VOBJ); + objIS.pushKV("txid", islock->txid.ToString()); + UniValue inputs(UniValue::VARR); + for (const auto out : islock->inputs) { + UniValue outpoint(UniValue::VOBJ); + outpoint.pushKV("txid", out.hash.ToString()); + outpoint.pushKV("vout", static_cast(out.n)); + inputs.push_back(outpoint); + } + objIS.pushKV("inputs", inputs); + objIS.pushKV("cycleHash", islock->cycleHash.ToString()); + objIS.pushKV("signature", islock->sig.ToString()); + { + CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); + ssTx << *islock; + objIS.pushKV("hex", HexStr(ssTx)); + } + result_arr.push_back(objIS); + } else { + result_arr.push_back("None"); + } + } + return result_arr; + +}, + }; +} + static RPCHelpMan gettxchainlocks() { return RPCHelpMan{ @@ -2088,6 +2169,7 @@ static const CRPCCommand commands[] = { "rawtransactions", &getassetunlockstatuses, }, { "rawtransactions", &getrawtransaction, }, { "rawtransactions", &getrawtransactionmulti, }, + { "rawtransactions", &getislocks, }, { "rawtransactions", &gettxchainlocks, }, { "rawtransactions", &createrawtransaction, }, { "rawtransactions", &decoderawtransaction, }, diff --git a/test/functional/interface_zmq_dash.py b/test/functional/interface_zmq_dash.py index b1f64e445da49..fa418b1213594 100755 --- a/test/functional/interface_zmq_dash.py +++ b/test/functional/interface_zmq_dash.py @@ -136,6 +136,8 @@ def run_test(self): self.zmq_context = zmq.Context() # Initialize the network self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0) + self.log.info("Test RPC hex getbestchainlock before any CL appeared") + assert_raises_rpc_error(-32603, "Unable to find any ChainLock", self.nodes[0].getbestchainlock) self.wait_for_sporks_same() self.activate_v19(expected_activation_height=900) self.log.info("Activated v19 at height:" + str(self.nodes[0].getblockcount())) @@ -263,6 +265,7 @@ def test_chainlock_publishers(self): assert_equal(uint256_to_string(zmq_chain_lock.blockHash), rpc_chain_lock_hash) assert_equal(zmq_chain_locked_block.hash, rpc_chain_lock_hash) assert_equal(zmq_chain_lock.sig.hex(), rpc_best_chain_lock_sig) + assert_equal(zmq_chain_lock.serialize().hex(), self.nodes[0].getbestchainlock()['hex']) # Unsubscribe from ChainLock messages self.unsubscribe(chain_lock_publishers) @@ -285,6 +288,7 @@ def test_instantsend_publishers(self): # Create two raw TXs, they will conflict with each other rpc_raw_tx_1 = self.create_raw_tx(self.nodes[0], self.nodes[0], 1, 1, 100) rpc_raw_tx_2 = self.create_raw_tx(self.nodes[0], self.nodes[0], 1, 1, 100) + assert_equal(['None'], self.nodes[0].getislocks([rpc_raw_tx_1['txid']])) # Send the first transaction and wait for the InstantLock rpc_raw_tx_1_hash = self.nodes[0].sendrawtransaction(rpc_raw_tx_1['hex']) self.wait_for_instantlock(rpc_raw_tx_1_hash, self.nodes[0]) @@ -304,6 +308,8 @@ def test_instantsend_publishers(self): assert_equal(zmq_tx_lock_tx.hash, rpc_raw_tx_1['txid']) zmq_tx_lock = msg_isdlock() zmq_tx_lock.deserialize(zmq_tx_lock_sig_stream) + assert_equal(rpc_raw_tx_1['txid'], self.nodes[0].getislocks([rpc_raw_tx_1['txid']])[0]['txid']) + assert_equal(zmq_tx_lock.serialize().hex(), self.nodes[0].getislocks([rpc_raw_tx_1['txid']])[0]['hex']) assert_equal(uint256_to_string(zmq_tx_lock.txid), rpc_raw_tx_1['txid']) # Try to send the second transaction. This must throw an RPC error because it conflicts with rpc_raw_tx_1 # which already got the InstantSend lock.