diff --git a/doc/release-notes-5776.md b/doc/release-notes-5776.md new file mode 100644 index 0000000000..ac1e102fa1 --- /dev/null +++ b/doc/release-notes-5776.md @@ -0,0 +1,11 @@ +Added RPC +-------- + +- `getassetunlockstatuses` RPC allows fetching of Asset Unlock txs by their withdrawal index. The RPC accepts an array of indexes and returns status for each index. +The possible outcomes per each index are: +- "chainlocked": If the Asset Unlock index is mined on a ChainLocked block. +- "mined": If no ChainLock information is available, and the Asset Unlock index is mined. +- "mempooled": If the Asset Unlock index is in the mempool. +- "unknown": If none of the above are valid. + +Note: This RPC is whitelisted for the Platform RPC user. diff --git a/src/init.cpp b/src/init.cpp index a1b8b85844..e3dcabee3e 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -340,6 +340,7 @@ void PrepareShutdown(NodeContext& node) llmq::quorumSnapshotManager.reset(); deterministicMNManager.reset(); creditPoolManager.reset(); + node.creditPoolManager = nullptr; node.mnhf_manager.reset(); node.evodb.reset(); } @@ -1926,6 +1927,7 @@ bool AppInitMain(const CoreContext& context, NodeContext& node, interfaces::Bloc node.mnhf_manager.reset(); node.mnhf_manager = std::make_unique(*node.evodb); + chainman.Reset(); chainman.InitializeChainstate(Assert(node.mempool.get()), *node.mnhf_manager, *node.evodb, llmq::chainLocksHandler, llmq::quorumInstantSendManager, llmq::quorumBlockProcessor); chainman.m_total_coinstip_cache = nCoinCacheUsage; @@ -1942,7 +1944,8 @@ bool AppInitMain(const CoreContext& context, NodeContext& node, interfaces::Bloc deterministicMNManager.reset(); deterministicMNManager.reset(new CDeterministicMNManager(chainman.ActiveChainstate(), *node.connman, *node.evodb)); creditPoolManager.reset(); - creditPoolManager.reset(new CCreditPoolManager(*node.evodb)); + creditPoolManager = std::make_unique(*node.evodb); + node.creditPoolManager = creditPoolManager.get(); llmq::quorumSnapshotManager.reset(); llmq::quorumSnapshotManager.reset(new llmq::CQuorumSnapshotManager(*node.evodb)); diff --git a/src/node/context.h b/src/node/context.h index ef9f88e6c3..7a24527f03 100644 --- a/src/node/context.h +++ b/src/node/context.h @@ -60,7 +60,7 @@ struct NodeContext { std::function rpc_interruption_point = [] {}; //! Dash std::unique_ptr llmq_ctx; - std::unique_ptr creditPoolManager; + CCreditPoolManager* creditPoolManager; std::unique_ptr mnhf_manager; std::unique_ptr cj_ctx; diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index ea81ace82f..649a4b5823 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -40,11 +41,13 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -336,6 +339,118 @@ static UniValue gettxchainlocks(const JSONRPCRequest& request) return result_arr; } +static void getassetunlockstatuses_help(const JSONRPCRequest& request) +{ + RPCHelpMan{ + "getassetunlockstatuses", + "\nReturns the status of given Asset Unlock indexes.\n", + { + {"indexes", RPCArg::Type::ARR, RPCArg::Optional::NO, "The Asset Unlock indexes (no more than 100)", + { + {"index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "An Asset Unlock index"}, + }, + }, + }, + RPCResult{ + RPCResult::Type::ARR, "", "Response is an array with the same size as the input txids", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::NUM, "index", "The Asset Unlock index"}, + {RPCResult::Type::STR, "status", "Status of the Asset Unlock index: {chainlocked|mined|mempooled|unknown}"}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("getassetunlockstatuses", "'[\"myindex\",...]'") + + HelpExampleRpc("getassetunlockstatuses", "[\"myindex\",...]") + }, + }.Check(request); +} + +static UniValue getassetunlockstatuses(const JSONRPCRequest& request) +{ + getassetunlockstatuses_help(request); + + const NodeContext& node = EnsureAnyNodeContext(request.context); + const CTxMemPool& mempool = EnsureMemPool(node); + const LLMQContext& llmq_ctx = EnsureLLMQContext(node); + const ChainstateManager& chainman = EnsureChainman(node); + + UniValue result_arr(UniValue::VARR); + const UniValue str_indexes = request.params[0].get_array(); + if (str_indexes.size() > 100) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Up to 100 indexes only"); + } + + if (g_txindex) { + g_txindex->BlockUntilSyncedToCurrentChain(); + } + + const CBlockIndex* pTipBlockIndex{WITH_LOCK(cs_main, return chainman.ActiveChain().Tip())}; + + if (!pTipBlockIndex) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "No blocks in chain"); + } + + const auto pBlockIndexBestCL = [&]() -> const CBlockIndex* { + if (llmq_ctx.clhandler->GetBestChainLock().IsNull()) { + // If no CL info is available, try to use CbTx CL information + if (const auto cbtx_best_cl = GetNonNullCoinbaseChainlock(pTipBlockIndex)) { + return pTipBlockIndex->GetAncestor(pTipBlockIndex->nHeight - cbtx_best_cl->second - 1); + } + } + return nullptr; + }(); + + // We need in 2 credit pools: at tip of chain and on best CL to know if tx is mined or chainlocked + // Sometimes that's two different blocks, sometimes not and we need to initialize 2nd creditPoolManager + std::optional poolCL = pBlockIndexBestCL ? + std::make_optional(node.creditPoolManager->GetCreditPool(pBlockIndexBestCL, Params().GetConsensus())) : + std::nullopt; + auto poolOnTip = [&]() -> std::optional { + if (pTipBlockIndex != pBlockIndexBestCL) { + return std::make_optional(node.creditPoolManager->GetCreditPool(pTipBlockIndex, Params().GetConsensus())); + } + return std::nullopt; + }(); + + for (const auto i : irange::range(str_indexes.size())) { + UniValue obj(UniValue::VOBJ); + uint64_t index{}; + if (!ParseUInt64(str_indexes[i].get_str(), &index)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid index"); + } + obj.pushKV("index", index); + auto status_to_push = [&]() -> std::string { + if (poolCL.has_value() && poolCL->indexes.Contains(index)) { + return "chainlocked"; + } + if (poolOnTip.has_value() && poolOnTip->indexes.Contains(index)) { + return "mined"; + } + bool is_mempooled = [&]() { + LOCK(mempool.cs); + return std::any_of(mempool.mapTx.begin(), mempool.mapTx.end(), [index](const CTxMemPoolEntry &e) { + if (e.GetTx().nType == CAssetUnlockPayload::SPECIALTX_TYPE) { + if (CAssetUnlockPayload assetUnlockTx; GetTxPayload(e.GetTx(), assetUnlockTx)) { + return index == assetUnlockTx.getIndex(); + } else { + throw JSONRPCError(RPC_TRANSACTION_ERROR, "bad-assetunlocktx-payload"); + } + } + return false; + }); + }(); + return is_mempooled ? "mempooled" : "unknown"; + }; + obj.pushKV("status", status_to_push()); + result_arr.push_back(obj); + } + + return result_arr; +} + static UniValue gettxoutproof(const JSONRPCRequest& request) { RPCHelpMan{"gettxoutproof", @@ -1757,6 +1872,7 @@ void RegisterRawTransactionRPCCommands(CRPCTable &t) static const CRPCCommand commands[] = { // category name actor (function) argNames // --------------------- ------------------------ ----------------------- ---------- + { "rawtransactions", "getassetunlockstatuses", &getassetunlockstatuses, {"indexes"} }, { "rawtransactions", "getrawtransaction", &getrawtransaction, {"txid","verbose","blockhash"} }, { "rawtransactions", "gettxchainlocks", &gettxchainlocks, {"txids"} }, { "rawtransactions", "createrawtransaction", &createrawtransaction, {"inputs","outputs","locktime"} }, diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index 989a4c16d6..4dfce58aa0 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -141,6 +141,7 @@ std::string CRPCTable::help(const std::string& strCommand, const std::string& st void CRPCTable::InitPlatformRestrictions() { mapPlatformRestrictions = { + {"getassetunlockstatuses", {}}, {"getbestblockhash", {}}, {"getblockhash", {}}, {"getblockcount", {}}, diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 8ce8fe760b..29bfccd987 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -169,6 +169,7 @@ BasicTestingSetup::BasicTestingSetup(const std::string& chainName, const std::ve connman = std::make_unique(0x1337, 0x1337, *m_node.addrman); llmq::quorumSnapshotManager.reset(new llmq::CQuorumSnapshotManager(*m_node.evodb)); creditPoolManager = std::make_unique(*m_node.evodb); + m_node.creditPoolManager = creditPoolManager.get(); static bool noui_connected = false; if (!noui_connected) { noui_connect(); @@ -182,6 +183,7 @@ BasicTestingSetup::~BasicTestingSetup() connman.reset(); llmq::quorumSnapshotManager.reset(); creditPoolManager.reset(); + m_node.creditPoolManager = nullptr; m_node.mnhf_manager.reset(); m_node.evodb.reset(); @@ -215,7 +217,9 @@ ChainTestingSetup::ChainTestingSetup(const std::string& chainName, const std::ve ::mmetaman = std::make_unique(/* load_cache */ false); ::netfulfilledman = std::make_unique(/* load_cache */ false); - m_node.creditPoolManager = std::make_unique(*m_node.evodb); + creditPoolManager = std::make_unique(*m_node.evodb); + m_node.creditPoolManager = creditPoolManager.get(); + // Start script-checking threads. Set g_parallel_script_checks to true so they are used. constexpr int script_check_threads = 2; @@ -226,7 +230,8 @@ ChainTestingSetup::ChainTestingSetup(const std::string& chainName, const std::ve ChainTestingSetup::~ChainTestingSetup() { m_node.scheduler->stop(); - m_node.creditPoolManager.reset(); + creditPoolManager.reset(); + m_node.creditPoolManager = nullptr; StopScriptCheckWorkerThreads(); GetMainSignals().FlushBackgroundCallbacks(); GetMainSignals().UnregisterBackgroundSignalScheduler(); diff --git a/test/functional/feature_asset_locks.py b/test/functional/feature_asset_locks.py index 465a363f2a..9c4ee716ed 100755 --- a/test/functional/feature_asset_locks.py +++ b/test/functional/feature_asset_locks.py @@ -330,6 +330,10 @@ def run_test(self): txid = self.send_tx(asset_unlock_tx) assert "assetUnlockTx" in node.getrawtransaction(txid, 1) + + indexes_statuses = self.nodes[0].getassetunlockstatuses(["101", "102", "300"]) + assert_equal([{'index': 101, 'status': 'mempooled'}, {'index': 102, 'status': 'unknown'}, {'index': 300, 'status': 'unknown'}], indexes_statuses) + self.mempool_size += 1 self.check_mempool_size() self.validate_credit_pool_balance(locked_1) @@ -502,6 +506,9 @@ def run_test(self): node.generate(1) self.sync_all() + indexes_statuses = self.nodes[0].getassetunlockstatuses(["101", "102", "103"]) + assert_equal([{'index': 101, 'status': 'mined'}, {'index': 102, 'status': 'mined'}, {'index': 103, 'status': 'unknown'}], indexes_statuses) + self.log.info("generate many blocks to be sure that mempool is empty after expiring txes...") self.slowly_generate_batch(60) self.log.info("Checking that credit pool is not changed...") diff --git a/test/functional/rpc_platform_filter.py b/test/functional/rpc_platform_filter.py index 383c83fa19..146948d479 100755 --- a/test/functional/rpc_platform_filter.py +++ b/test/functional/rpc_platform_filter.py @@ -56,7 +56,8 @@ def test_command(method, params, auth, expexted_status, should_not_match=False): assert_equal(resp.status, expexted_status) conn.close() - whitelisted = ["getbestblockhash", + whitelisted = ["getassetunlockstatuses", + "getbestblockhash", "getblockhash", "getblockcount", "getbestchainlock",