rpc: Always return per-wtxid entries in submitpackage tx-results
What changed, and why it matters
This commit fixes a minor bug in Bitcoin Core's submitpackage RPC. Previously, when a package of transactions failed early, the RPC would label the failure as 'unevaluated' but wouldn't actually include each transaction's result in the response, making it hard for callers to know which transactions were affected. Now it always returns a per-transaction entry with a clearer 'package-not-validated' message. This is a usability/API correctness fix, not a security vulnerability.
No security action required; treat as a normal bugfix/behavioral improvement. Callers using submitpackage should be aware that tx-results now always contains all submitted wtxids and that the error string for early-abort packages is now 'package-not-validated' rather than 'unevaluated'.
Security signals we found
No memory safety, cryptographic, consensus, or authorization issues evident
Change is an API response completeness/correctness fix
Adds a non-fatal assertion (CHECK_NONFATAL) to enforce an internal invariant
No attacker-controlled input is trusted beyond existing RPC transaction hex handling
Evidence from the diff
The patch modifies src/rpc/mempool.cpp so that submitpackage’s tx-results object always contains an entry for every submitted wtxid. When package validation aborts before per-transaction processing, the code now inserts a placeholder result with error ‘package-not-validated’ instead of the previous behavior of setting ‘error’: ‘unevaluated’ internally and then continuing without adding the entry. It also adds a CHECK_NONFATAL assertion enforcing the all-or-none invariant for m_tx_results, updates RPC help text, and adjusts functional tests accordingly.
Changed components
Bitcoin Core RPC: submitpackage in src/rpc/mempool.cppFunctional test rpc_packages.pyInspect captured patch +22 / −7
diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp
index 919c3046..147af369 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 325a29c4..2249fe49 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
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.