diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index cc25bff621..6b686d7ae1 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -107,6 +107,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "gettransaction", 1, "include_watchonly" }, { "gettransaction", 2, "verbose" }, { "getrawtransaction", 1, "verbose" }, + { "gettxchainlocks", 0, "txids" }, { "createrawtransaction", 0, "inputs" }, { "createrawtransaction", 1, "outputs" }, { "createrawtransaction", 2, "locktime" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 3d99f2aae2..15332e71d4 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include @@ -261,6 +262,80 @@ static UniValue getrawtransaction(const JSONRPCRequest& request) return result; } +static UniValue gettxchainlocks(const JSONRPCRequest& request) +{ + RPCHelpMan{ + "gettxchainlocks", + "\nReturns the block height each transaction was mined at and whether it is chainlocked or not.\n", + { + {"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::NUM, "height", "The block height"}, + {RPCResult::Type::BOOL, "chainlock", "Chainlock status for the block containing the transaction"}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("gettxchainlocks", "'[\"mytxid\",...]'") + + HelpExampleRpc("gettxchainlocks", "[\"mytxid\",...]") + }, + }.Check(request); + + const NodeContext& node = EnsureAnyNodeContext(request.context); + const LLMQContext& llmq_ctx = EnsureLLMQContext(node); + const ChainstateManager& chainman = EnsureChainman(node); + const CChainState& active_chainstate = chainman.ActiveChainstate(); + + 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"); + } + + if (g_txindex) { + g_txindex->BlockUntilSyncedToCurrentChain(); + } + + for (const auto idx : irange::range(txids.size())) { + UniValue result(UniValue::VOBJ); + const uint256 txid(ParseHashV(txids[idx], "txid")); + if (txid == Params().GenesisBlock().hashMerkleRoot) { + // Special exception for the genesis block coinbase transaction + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "The genesis block coinbase is not considered an ordinary transaction and cannot be retrieved"); + } + + uint256 hash_block; + int height{-1}; + bool chainLock{false}; + + GetTransaction(nullptr, nullptr, txid, Params().GetConsensus(), hash_block); + + if (!hash_block.IsNull()) { + LOCK(cs_main); + const CBlockIndex* pindex = active_chainstate.m_blockman.LookupBlockIndex(hash_block); + if (pindex && active_chainstate.m_chain.Contains(pindex)) { + height = pindex->nHeight; + } + } + if (height != -1) { + chainLock = llmq_ctx.clhandler->HasChainLock(height, hash_block); + } + result.pushKV("height", height); + result.pushKV("chainlock", chainLock); + result_arr.push_back(result); + } + return result_arr; +} + static UniValue gettxoutproof(const JSONRPCRequest& request) { RPCHelpMan{"gettxoutproof", @@ -1683,6 +1758,7 @@ static const CRPCCommand commands[] = { // category name actor (function) argNames // --------------------- ------------------------ ----------------------- ---------- { "rawtransactions", "getrawtransaction", &getrawtransaction, {"txid","verbose","blockhash"} }, + { "rawtransactions", "gettxchainlocks", &gettxchainlocks, {"txids"} }, { "rawtransactions", "createrawtransaction", &createrawtransaction, {"inputs","outputs","locktime"} }, { "rawtransactions", "decoderawtransaction", &decoderawtransaction, {"hexstring"} }, { "rawtransactions", "decodescript", &decodescript, {"hexstring"} }, diff --git a/src/test/rpc_tests.cpp b/src/test/rpc_tests.cpp index 5e73118558..3eeb868de9 100644 --- a/src/test/rpc_tests.cpp +++ b/src/test/rpc_tests.cpp @@ -115,6 +115,10 @@ BOOST_AUTO_TEST_CASE(rpc_rawparams) BOOST_CHECK_THROW(CallRPC("getrawtransaction not_hex"), std::runtime_error); BOOST_CHECK_THROW(CallRPC("getrawtransaction a3b807410df0b60fcb9736768df5823938b2f838694939ba45f3c0a1bff150ed not_int"), std::runtime_error); + BOOST_CHECK_THROW(CallRPC("gettxchainlocks"), std::runtime_error); + BOOST_CHECK_THROW(CallRPC("gettxchainlocks not_array"), std::runtime_error); + BOOST_CHECK_THROW(CallRPC("gettxchainlocks [] extra"), std::runtime_error); + BOOST_CHECK_THROW(CallRPC("createrawtransaction"), std::runtime_error); BOOST_CHECK_THROW(CallRPC("createrawtransaction null null"), std::runtime_error); BOOST_CHECK_THROW(CallRPC("createrawtransaction not_array"), std::runtime_error); diff --git a/test/functional/rpc_verifychainlock.py b/test/functional/rpc_verifychainlock.py index fcb3132129..a11aba3adc 100755 --- a/test/functional/rpc_verifychainlock.py +++ b/test/functional/rpc_verifychainlock.py @@ -4,12 +4,14 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. from test_framework.test_framework import DashTestFramework -from test_framework.util import assert_raises_rpc_error +from test_framework.util import assert_equal, assert_raises_rpc_error ''' rpc_verifychainlock.py -Test verifychainlock rpc +Test the following RPC: + - gettxchainlocks + - verifychainlock ''' @@ -57,6 +59,16 @@ class RPCVerifyChainLockTest(DashTestFramework): assert node0.verifychainlock(block_hash, chainlock_signature, height) assert node1.verifychainlock(block_hash, chainlock_signature, height) + node1.generate(1) + height1 = node1.getblockcount() + tx0 = node0.getblock(node0.getbestblockhash())['tx'][0] + tx1 = node1.getblock(node1.getbestblockhash())['tx'][0] + locks0 = node0.gettxchainlocks([tx0, tx1]) + locks1 = node1.gettxchainlocks([tx0, tx1]) + unknown_cl_helper = {'height': -1, 'chainlock': False} + assert_equal(locks0, [{'height': height, 'chainlock': True}, unknown_cl_helper]) + assert_equal(locks1, [unknown_cl_helper, {'height': height1, 'chainlock': False}]) + if __name__ == '__main__': RPCVerifyChainLockTest().main()