Skip to content

Commit 6b3927f

Browse files
committed
Merge bitcoin#28848: bugfix, Change up submitpackage results to return results for all transactions
f23ba24 test_submitpackage: only make a chain of 3 txns (Greg Sanders) e67a345 doc: submitpackage vsize results are sigops-adjusted (Greg Sanders) b67db52 RPC submitpackage: change return format to allow partial errors (Greg Sanders) Pull request description: This was prompted by errors being returned that didn't "make any sense" to me, because it would for example return a "fee too low" error, when the "real" error was the child had something invalid, which disallowed CPFP evaluation. Rather than make judgment calls on what error is important(which is currently just return the "first"!), we simply return all errors and let the callers determine what's best. Added a top level `package_msg` for quick eye-balling of general success of the package. This PR also fixes a couple bugs: 1) Currently we don't actually broadcast a transaction, even if it was entered into our mempool, if a subsequent transaction causes `PKG_TX` failure. 2) "other-wtxid" is uncovered by tests, but IIUC was previously required to return "fees" and "vsize" results, but did not. I just make those results optional. ACKs for top commit: Sjors: Light re-utACK f23ba24 achow101: ACK f23ba24 glozow: utACK f23ba24, thanks for taking the suggestions Tree-SHA512: ebfd716a4fed9e8c2dea3d2181ba6a6171b06718d29ac2324c67b7a30b374d199f7e1739f91ab5d036be172d0479de9bc89c32263ee62143c0338b9b622d0cca
2 parents 498994b + f23ba24 commit 6b3927f

File tree

4 files changed

+79
-32
lines changed

4 files changed

+79
-32
lines changed

src/rpc/mempool.cpp

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,7 @@ static RPCHelpMan submitpackage()
818818
return RPCHelpMan{"submitpackage",
819819
"Submit a package of raw transactions (serialized, hex-encoded) to local node.\n"
820820
"The package must consist of a child with its parents, and none of the parents may depend on one another.\n"
821-
"The package will be validated according to consensus and mempool policy rules. If all transactions pass, they will be accepted to mempool.\n"
821+
"The package will be validated according to consensus and mempool policy rules. If any transaction passes, it will be accepted to mempool.\n"
822822
"This RPC is experimental and the interface may be unstable. Refer to doc/policy/packages.md for documentation on package policies.\n"
823823
"Warning: successful submission does not mean the transactions will propagate throughout the network.\n"
824824
,
@@ -832,19 +832,21 @@ static RPCHelpMan submitpackage()
832832
RPCResult{
833833
RPCResult::Type::OBJ, "", "",
834834
{
835+
{RPCResult::Type::STR, "package_msg", "The transaction package result message. \"success\" indicates all transactions were accepted into or are already in the mempool."},
835836
{RPCResult::Type::OBJ_DYN, "tx-results", "transaction results keyed by wtxid",
836837
{
837838
{RPCResult::Type::OBJ, "wtxid", "transaction wtxid", {
838839
{RPCResult::Type::STR_HEX, "txid", "The transaction hash in hex"},
839840
{RPCResult::Type::STR_HEX, "other-wtxid", /*optional=*/true, "The wtxid of a different transaction with the same txid but different witness found in the mempool. This means the submitted transaction was ignored."},
840-
{RPCResult::Type::NUM, "vsize", "Virtual transaction size as defined in BIP 141."},
841-
{RPCResult::Type::OBJ, "fees", "Transaction fees", {
841+
{RPCResult::Type::NUM, "vsize", /*optional=*/true, "Sigops-adjusted virtual transaction size."},
842+
{RPCResult::Type::OBJ, "fees", /*optional=*/true, "Transaction fees", {
842843
{RPCResult::Type::STR_AMOUNT, "base", "transaction fee in " + CURRENCY_UNIT},
843844
{RPCResult::Type::STR_AMOUNT, "effective-feerate", /*optional=*/true, "if the transaction was not already in the mempool, the effective feerate in " + CURRENCY_UNIT + " per KvB. For example, the package feerate and/or feerate with modified fees from prioritisetransaction."},
844845
{RPCResult::Type::ARR, "effective-includes", /*optional=*/true, "if effective-feerate is provided, the wtxids of the transactions whose fees and vsizes are included in effective-feerate.",
845846
{{RPCResult::Type::STR_HEX, "", "transaction wtxid in hex"},
846847
}},
847848
}},
849+
{RPCResult::Type::STR, "error", /*optional=*/true, "The transaction error string, if it was rejected by the mempool"},
848850
}}
849851
}},
850852
{RPCResult::Type::ARR, "replaced-transactions", /*optional=*/true, "List of txids of replaced transactions",
@@ -884,57 +886,77 @@ static RPCHelpMan submitpackage()
884886
Chainstate& chainstate = EnsureChainman(node).ActiveChainstate();
885887
const auto package_result = WITH_LOCK(::cs_main, return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/ false));
886888

887-
// First catch any errors.
889+
std::string package_msg = "success";
890+
891+
// First catch package-wide errors, continue if we can
888892
switch(package_result.m_state.GetResult()) {
889-
case PackageValidationResult::PCKG_RESULT_UNSET: break;
890-
case PackageValidationResult::PCKG_POLICY:
893+
case PackageValidationResult::PCKG_RESULT_UNSET:
891894
{
892-
throw JSONRPCTransactionError(TransactionError::INVALID_PACKAGE,
893-
package_result.m_state.GetRejectReason());
895+
// Belt-and-suspenders check; everything should be successful here
896+
CHECK_NONFATAL(package_result.m_tx_results.size() == txns.size());
897+
for (const auto& tx : txns) {
898+
CHECK_NONFATAL(mempool.exists(GenTxid::Txid(tx->GetHash())));
899+
}
900+
break;
894901
}
895902
case PackageValidationResult::PCKG_MEMPOOL_ERROR:
896903
{
904+
// This only happens with internal bug; user should stop and report
897905
throw JSONRPCTransactionError(TransactionError::MEMPOOL_ERROR,
898906
package_result.m_state.GetRejectReason());
899907
}
908+
case PackageValidationResult::PCKG_POLICY:
900909
case PackageValidationResult::PCKG_TX:
901910
{
902-
for (const auto& tx : txns) {
903-
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
904-
if (it != package_result.m_tx_results.end() && it->second.m_state.IsInvalid()) {
905-
throw JSONRPCTransactionError(TransactionError::MEMPOOL_REJECTED,
906-
strprintf("%s failed: %s", tx->GetHash().ToString(), it->second.m_state.GetRejectReason()));
907-
}
908-
}
909-
// If a PCKG_TX error was returned, there must have been an invalid transaction.
910-
NONFATAL_UNREACHABLE();
911+
// Package-wide error we want to return, but we also want to return individual responses
912+
package_msg = package_result.m_state.GetRejectReason();
913+
CHECK_NONFATAL(package_result.m_tx_results.size() == txns.size() ||
914+
package_result.m_tx_results.empty());
915+
break;
911916
}
912917
}
918+
913919
size_t num_broadcast{0};
914920
for (const auto& tx : txns) {
921+
// We don't want to re-submit the txn for validation in BroadcastTransaction
922+
if (!mempool.exists(GenTxid::Txid(tx->GetHash()))) {
923+
continue;
924+
}
925+
926+
// We do not expect an error here; we are only broadcasting things already/still in mempool
915927
std::string err_string;
916928
const auto err = BroadcastTransaction(node, tx, err_string, /*max_tx_fee=*/0, /*relay=*/true, /*wait_callback=*/true);
917929
if (err != TransactionError::OK) {
918930
throw JSONRPCTransactionError(err,
919-
strprintf("transaction broadcast failed: %s (all transactions were submitted, %d transactions were broadcast successfully)",
931+
strprintf("transaction broadcast failed: %s (%d transactions were broadcast successfully)",
920932
err_string, num_broadcast));
921933
}
922934
num_broadcast++;
923935
}
936+
924937
UniValue rpc_result{UniValue::VOBJ};
938+
rpc_result.pushKV("package_msg", package_msg);
925939
UniValue tx_result_map{UniValue::VOBJ};
926940
std::set<uint256> replaced_txids;
927941
for (const auto& tx : txns) {
928-
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
929-
CHECK_NONFATAL(it != package_result.m_tx_results.end());
930942
UniValue result_inner{UniValue::VOBJ};
931943
result_inner.pushKV("txid", tx->GetHash().GetHex());
944+
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
945+
if (it == package_result.m_tx_results.end()) {
946+
// No results, report error and continue
947+
result_inner.pushKV("error", "unevaluated");
948+
continue;
949+
}
932950
const auto& tx_result = it->second;
933-
if (it->second.m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS) {
951+
switch(it->second.m_result_type) {
952+
case MempoolAcceptResult::ResultType::DIFFERENT_WITNESS:
934953
result_inner.pushKV("other-wtxid", it->second.m_other_wtxid.value().GetHex());
935-
}
936-
if (it->second.m_result_type == MempoolAcceptResult::ResultType::VALID ||
937-
it->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY) {
954+
break;
955+
case MempoolAcceptResult::ResultType::INVALID:
956+
result_inner.pushKV("error", it->second.m_state.ToString());
957+
break;
958+
case MempoolAcceptResult::ResultType::VALID:
959+
case MempoolAcceptResult::ResultType::MEMPOOL_ENTRY:
938960
result_inner.pushKV("vsize", int64_t{it->second.m_vsize.value()});
939961
UniValue fees(UniValue::VOBJ);
940962
fees.pushKV("base", ValueFromAmount(it->second.m_base_fees.value()));
@@ -955,6 +977,7 @@ static RPCHelpMan submitpackage()
955977
replaced_txids.insert(ptx->GetHash());
956978
}
957979
}
980+
break;
958981
}
959982
tx_result_map.pushKV(tx->GetWitnessHash().GetHex(), result_inner);
960983
}

test/functional/mempool_limit.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,9 @@ def test_rbf_carveout_disallowed(self):
125125
utxo_to_spend=tx_B["new_utxo"],
126126
confirmed_only=True
127127
)
128-
129-
assert_raises_rpc_error(-26, "too-long-mempool-chain", node.submitpackage, [tx_B["hex"], tx_C["hex"]])
128+
res = node.submitpackage([tx_B["hex"], tx_C["hex"]])
129+
assert_equal(res["package_msg"], "transaction failed")
130+
assert "too-long-mempool-chain" in res["tx-results"][tx_C["wtxid"]]["error"]
130131

131132
def test_mid_package_eviction(self):
132133
node = self.nodes[0]
@@ -205,7 +206,7 @@ def test_mid_package_eviction(self):
205206

206207
# Package should be submitted, temporarily exceeding maxmempool, and then evicted.
207208
with node.assert_debug_log(expected_msgs=["rolling minimum fee bumped"]):
208-
assert_raises_rpc_error(-26, "mempool full", node.submitpackage, package_hex)
209+
assert_equal(node.submitpackage(package_hex)["package_msg"], "transaction failed")
209210

210211
# Maximum size must never be exceeded.
211212
assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
@@ -273,7 +274,9 @@ def test_mid_package_replacement(self):
273274
package_hex = [cpfp_parent["hex"], replacement_tx["hex"], child["hex"]]
274275

275276
# Package should be submitted, temporarily exceeding maxmempool, and then evicted.
276-
assert_raises_rpc_error(-26, "bad-txns-inputs-missingorspent", node.submitpackage, package_hex)
277+
res = node.submitpackage(package_hex)
278+
assert_equal(res["package_msg"], "transaction failed")
279+
assert len([tx_res for _, tx_res in res["tx-results"].items() if "error" in tx_res and tx_res["error"] == "bad-txns-inputs-missingorspent"])
277280

278281
# Maximum size must never be exceeded.
279282
assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
@@ -321,6 +324,7 @@ def run_test(self):
321324
package_txns.append(tx_child)
322325

323326
submitpackage_result = node.submitpackage([tx["hex"] for tx in package_txns])
327+
assert_equal(submitpackage_result["package_msg"], "success")
324328

325329
rich_parent_result = submitpackage_result["tx-results"][tx_rich["wtxid"]]
326330
poor_parent_result = submitpackage_result["tx-results"][tx_poor["wtxid"]]
@@ -366,7 +370,9 @@ def run_test(self):
366370
assert_greater_than(worst_feerate_btcvb, (parent_fee + child_fee) / (tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()))
367371
assert_greater_than(mempoolmin_feerate, (parent_fee) / (tx_parent_just_below["tx"].get_vsize()))
368372
assert_greater_than((parent_fee + child_fee) / (tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()), mempoolmin_feerate / 1000)
369-
assert_raises_rpc_error(-26, "mempool full", node.submitpackage, [tx_parent_just_below["hex"], tx_child_just_above["hex"]])
373+
res = node.submitpackage([tx_parent_just_below["hex"], tx_child_just_above["hex"]])
374+
for wtxid in [tx_parent_just_below["wtxid"], tx_child_just_above["wtxid"]]:
375+
assert_equal(res["tx-results"][wtxid]["error"], "mempool full")
370376

371377
self.log.info('Test passing a value below the minimum (5 MB) to -maxmempool throws an error')
372378
self.stop_node(0)

test/functional/mempool_sigoplimit.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
assert_equal,
3535
assert_greater_than,
3636
assert_greater_than_or_equal,
37-
assert_raises_rpc_error,
3837
)
3938
from test_framework.wallet import MiniWallet
4039
from test_framework.wallet_util import generate_keypair
@@ -169,7 +168,8 @@ def create_bare_multisig_tx(utxo_to_spend=None):
169168
assert_equal([x["package-error"] for x in packet_test], ["package-mempool-limits", "package-mempool-limits"])
170169

171170
# When we actually try to submit, the parent makes it into the mempool, but the child would exceed ancestor vsize limits
172-
assert_raises_rpc_error(-26, "too-long-mempool-chain", self.nodes[0].submitpackage, [tx_parent.serialize().hex(), tx_child.serialize().hex()])
171+
res = self.nodes[0].submitpackage([tx_parent.serialize().hex(), tx_child.serialize().hex()])
172+
assert "too-long-mempool-chain" in res["tx-results"][tx_child.getwtxid()]["error"]
173173
assert tx_parent.rehash() in self.nodes[0].getrawmempool()
174174

175175
# Transactions are tiny in weight

test/functional/rpc_packages.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ def test_submit_child_with_parents(self, num_parents, partial_submit):
304304
submitpackage_result = node.submitpackage(package=[tx["hex"] for tx in package_txns])
305305

306306
# Check that each result is present, with the correct size and fees
307+
assert_equal(submitpackage_result["package_msg"], "success")
307308
for package_txn in package_txns:
308309
tx = package_txn["tx"]
309310
assert tx.getwtxid() in submitpackage_result["tx-results"]
@@ -334,9 +335,26 @@ def test_submitpackage(self):
334335

335336
self.log.info("Submitpackage only allows packages of 1 child with its parents")
336337
# Chain of 3 transactions has too many generations
337-
chain_hex = [t["hex"] for t in self.wallet.create_self_transfer_chain(chain_length=25)]
338+
legacy_pool = node.getrawmempool()
339+
chain_hex = [t["hex"] for t in self.wallet.create_self_transfer_chain(chain_length=3)]
338340
assert_raises_rpc_error(-25, "package topology disallowed", node.submitpackage, chain_hex)
341+
assert_equal(legacy_pool, node.getrawmempool())
339342

343+
# Create a transaction chain such as only the parent gets accepted (by making the child's
344+
# version non-standard). Make sure the parent does get broadcast.
345+
self.log.info("If a package is partially submitted, transactions included in mempool get broadcast")
346+
peer = node.add_p2p_connection(P2PTxInvStore())
347+
txs = self.wallet.create_self_transfer_chain(chain_length=2)
348+
bad_child = tx_from_hex(txs[1]["hex"])
349+
bad_child.nVersion = -1
350+
hex_partial_acceptance = [txs[0]["hex"], bad_child.serialize().hex()]
351+
res = node.submitpackage(hex_partial_acceptance)
352+
assert_equal(res["package_msg"], "transaction failed")
353+
first_wtxid = txs[0]["tx"].getwtxid()
354+
assert "error" not in res["tx-results"][first_wtxid]
355+
sec_wtxid = bad_child.getwtxid()
356+
assert_equal(res["tx-results"][sec_wtxid]["error"], "version")
357+
peer.wait_for_broadcast([first_wtxid])
340358

341359
if __name__ == "__main__":
342360
RPCPackagesTest().main()

0 commit comments

Comments
 (0)