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