From cad9a7fd7370f6df38370569f9d2de6ac6b7137a Mon Sep 17 00:00:00 2001 From: John Moffett Date: Thu, 18 Sep 2025 09:00:31 -0400 Subject: [PATCH] rpc: Always return per-wtxid entries in submitpackage tx-results When submitpackage produced no per-transaction result for a member, the RPC previously set "error": "unevaluated" but then continued without inserting the entry into tx-results, making it impossible for callers to know which wtxids were unevaluated. Insert the placeholder result before continuing, update help text, and adjust functional tests to expect entries for all submitted wtxids. --- src/rpc/mempool.cpp | 15 ++++++++++----- test/functional/rpc_packages.py | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp index 919c30464dd..147af369d34 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -955,7 +955,7 @@ static RPCHelpMan submitpackage() RPCResult::Type::OBJ, "", "", { {RPCResult::Type::STR, "package_msg", "The transaction package result message. \"success\" indicates all transactions were accepted into or are already in the mempool."}, - {RPCResult::Type::OBJ_DYN, "tx-results", "transaction results keyed by wtxid", + {RPCResult::Type::OBJ_DYN, "tx-results", "The transaction results keyed by wtxid. An entry is returned for every submitted wtxid.", { {RPCResult::Type::OBJ, "wtxid", "transaction wtxid", { {RPCResult::Type::STR_HEX, "txid", "The transaction hash in hex"}, @@ -968,7 +968,7 @@ static RPCHelpMan submitpackage() {{RPCResult::Type::STR_HEX, "", "transaction wtxid in hex"}, }}, }}, - {RPCResult::Type::STR, "error", /*optional=*/true, "The transaction error string, if it was rejected by the mempool"}, + {RPCResult::Type::STR, "error", /*optional=*/true, "Error string if rejected from mempool, or \"package-not-validated\" when the package aborts before any per-tx processing."}, }} }}, {RPCResult::Type::ARR, "replaced-transactions", /*optional=*/true, "List of txids of replaced transactions", @@ -1082,10 +1082,15 @@ static RPCHelpMan submitpackage() for (const auto& tx : txns) { UniValue result_inner{UniValue::VOBJ}; result_inner.pushKV("txid", tx->GetHash().GetHex()); + const auto wtxid_hex = tx->GetWitnessHash().GetHex(); auto it = package_result.m_tx_results.find(tx->GetWitnessHash()); if (it == package_result.m_tx_results.end()) { - // No results, report error and continue - result_inner.pushKV("error", "unevaluated"); + // No per-tx result for this wtxid + // Current invariant: per-tx results are all-or-none (every member or empty on package abort). + // If any exist yet this one is missing, it's an unexpected partial map. + CHECK_NONFATAL(package_result.m_tx_results.empty()); + result_inner.pushKV("error", "package-not-validated"); + tx_result_map.pushKV(wtxid_hex, std::move(result_inner)); continue; } const auto& tx_result = it->second; @@ -1118,7 +1123,7 @@ static RPCHelpMan submitpackage() } break; } - tx_result_map.pushKV(tx->GetWitnessHash().GetHex(), std::move(result_inner)); + tx_result_map.pushKV(wtxid_hex, std::move(result_inner)); } rpc_result.pushKV("tx-results", std::move(tx_result_map)); UniValue replaced_list(UniValue::VARR); diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py index 325a29c4efc..2249fe49b56 100755 --- a/test/functional/rpc_packages.py +++ b/test/functional/rpc_packages.py @@ -263,13 +263,23 @@ class RPCPackagesTest(BitcoinTestFramework): ]) submitres = node.submitpackage([tx1["hex"], tx2["hex"], tx_child["hex"]]) - assert_equal(submitres, {'package_msg': 'conflict-in-package', 'tx-results': {}, 'replaced-transactions': []}) + expected = { + tx1["wtxid"]: {"txid": tx1["txid"], "error": "package-not-validated"}, + tx2["wtxid"]: {"txid": tx2["txid"], "error": "package-not-validated"}, + tx_child["wtxid"]: {"txid": tx_child["txid"], "error": "package-not-validated"}, + } + assert_equal(submitres, {"package_msg": "conflict-in-package", "tx-results": expected,"replaced-transactions": []}) # Submit tx1 to mempool, then try the same package again node.sendrawtransaction(tx1["hex"]) submitres = node.submitpackage([tx1["hex"], tx2["hex"], tx_child["hex"]]) - assert_equal(submitres, {'package_msg': 'conflict-in-package', 'tx-results': {}, 'replaced-transactions': []}) + expected = { + tx1["wtxid"]: {"txid": tx1["txid"], "error": "package-not-validated"}, + tx2["wtxid"]: {"txid": tx2["txid"], "error": "package-not-validated"}, + tx_child["wtxid"]: {"txid": tx_child["txid"], "error": "package-not-validated"}, + } + assert_equal(submitres, {"package_msg": "conflict-in-package", "tx-results": expected,"replaced-transactions": []}) assert tx_child["txid"] not in node.getrawmempool() # without the in-mempool ancestor tx1 included in the call, tx2 can be submitted, but