mirror of
https://github.com/bitcoin/bitcoin.git
synced 2024-11-20 02:25:40 +01:00
RPC submitpackage: change return format to allow partial errors
Behavior prior to this commit allows some transactions to enter into the local mempool but not be reported to the user when encountering a PackageValidationResult::PCKG_TX result. This is further compounded with the fact that any transactions submitted to the mempool during this call would also not be relayed to peers, resulting in unexpected behavior. Fix this by, if encountering a package error, reporting all wtxids, along with a new error field, and broadcasting every transaction that was found in the mempool after submission. Note that this also changes fees and vsize to optional, which should also remove an issue with other-wtxid cases.
This commit is contained in:
parent
1fdd832842
commit
b67db52c39
@ -822,7 +822,7 @@ static RPCHelpMan submitpackage()
|
||||
return RPCHelpMan{"submitpackage",
|
||||
"Submit a package of raw transactions (serialized, hex-encoded) to local node.\n"
|
||||
"The package must consist of a child with its parents, and none of the parents may depend on one another.\n"
|
||||
"The package will be validated according to consensus and mempool policy rules. If all transactions pass, they will be accepted to mempool.\n"
|
||||
"The package will be validated according to consensus and mempool policy rules. If any transaction passes, it will be accepted to mempool.\n"
|
||||
"This RPC is experimental and the interface may be unstable. Refer to doc/policy/packages.md for documentation on package policies.\n"
|
||||
"Warning: successful submission does not mean the transactions will propagate throughout the network.\n"
|
||||
,
|
||||
@ -836,19 +836,21 @@ static RPCHelpMan submitpackage()
|
||||
RPCResult{
|
||||
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, "wtxid", "transaction wtxid", {
|
||||
{RPCResult::Type::STR_HEX, "txid", "The transaction hash in hex"},
|
||||
{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."},
|
||||
{RPCResult::Type::NUM, "vsize", "Virtual transaction size as defined in BIP 141."},
|
||||
{RPCResult::Type::OBJ, "fees", "Transaction fees", {
|
||||
{RPCResult::Type::NUM, "vsize", /*optional=*/true, "Virtual transaction size as defined in BIP 141."},
|
||||
{RPCResult::Type::OBJ, "fees", /*optional=*/true, "Transaction fees", {
|
||||
{RPCResult::Type::STR_AMOUNT, "base", "transaction fee in " + CURRENCY_UNIT},
|
||||
{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."},
|
||||
{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.",
|
||||
{{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::ARR, "replaced-transactions", /*optional=*/true, "List of txids of replaced transactions",
|
||||
@ -888,57 +890,77 @@ static RPCHelpMan submitpackage()
|
||||
Chainstate& chainstate = EnsureChainman(node).ActiveChainstate();
|
||||
const auto package_result = WITH_LOCK(::cs_main, return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/ false));
|
||||
|
||||
// First catch any errors.
|
||||
std::string package_msg = "success";
|
||||
|
||||
// First catch package-wide errors, continue if we can
|
||||
switch(package_result.m_state.GetResult()) {
|
||||
case PackageValidationResult::PCKG_RESULT_UNSET: break;
|
||||
case PackageValidationResult::PCKG_POLICY:
|
||||
case PackageValidationResult::PCKG_RESULT_UNSET:
|
||||
{
|
||||
throw JSONRPCTransactionError(TransactionError::INVALID_PACKAGE,
|
||||
package_result.m_state.GetRejectReason());
|
||||
// Belt-and-suspenders check; everything should be successful here
|
||||
CHECK_NONFATAL(package_result.m_tx_results.size() == txns.size());
|
||||
for (const auto& tx : txns) {
|
||||
CHECK_NONFATAL(mempool.exists(GenTxid::Txid(tx->GetHash())));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PackageValidationResult::PCKG_MEMPOOL_ERROR:
|
||||
{
|
||||
// This only happens with internal bug; user should stop and report
|
||||
throw JSONRPCTransactionError(TransactionError::MEMPOOL_ERROR,
|
||||
package_result.m_state.GetRejectReason());
|
||||
}
|
||||
case PackageValidationResult::PCKG_POLICY:
|
||||
case PackageValidationResult::PCKG_TX:
|
||||
{
|
||||
for (const auto& tx : txns) {
|
||||
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
|
||||
if (it != package_result.m_tx_results.end() && it->second.m_state.IsInvalid()) {
|
||||
throw JSONRPCTransactionError(TransactionError::MEMPOOL_REJECTED,
|
||||
strprintf("%s failed: %s", tx->GetHash().ToString(), it->second.m_state.GetRejectReason()));
|
||||
}
|
||||
}
|
||||
// If a PCKG_TX error was returned, there must have been an invalid transaction.
|
||||
NONFATAL_UNREACHABLE();
|
||||
// Package-wide error we want to return, but we also want to return individual responses
|
||||
package_msg = package_result.m_state.GetRejectReason();
|
||||
CHECK_NONFATAL(package_result.m_tx_results.size() == txns.size() ||
|
||||
package_result.m_tx_results.empty());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
size_t num_broadcast{0};
|
||||
for (const auto& tx : txns) {
|
||||
// We don't want to re-submit the txn for validation in BroadcastTransaction
|
||||
if (!mempool.exists(GenTxid::Txid(tx->GetHash()))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We do not expect an error here; we are only broadcasting things already/still in mempool
|
||||
std::string err_string;
|
||||
const auto err = BroadcastTransaction(node, tx, err_string, /*max_tx_fee=*/0, /*relay=*/true, /*wait_callback=*/true);
|
||||
if (err != TransactionError::OK) {
|
||||
throw JSONRPCTransactionError(err,
|
||||
strprintf("transaction broadcast failed: %s (all transactions were submitted, %d transactions were broadcast successfully)",
|
||||
strprintf("transaction broadcast failed: %s (%d transactions were broadcast successfully)",
|
||||
err_string, num_broadcast));
|
||||
}
|
||||
num_broadcast++;
|
||||
}
|
||||
|
||||
UniValue rpc_result{UniValue::VOBJ};
|
||||
rpc_result.pushKV("package_msg", package_msg);
|
||||
UniValue tx_result_map{UniValue::VOBJ};
|
||||
std::set<uint256> replaced_txids;
|
||||
for (const auto& tx : txns) {
|
||||
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
|
||||
CHECK_NONFATAL(it != package_result.m_tx_results.end());
|
||||
UniValue result_inner{UniValue::VOBJ};
|
||||
result_inner.pushKV("txid", tx->GetHash().GetHex());
|
||||
const auto& tx_result = it->second;
|
||||
if (it->second.m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS) {
|
||||
result_inner.pushKV("other-wtxid", it->second.m_other_wtxid.value().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");
|
||||
continue;
|
||||
}
|
||||
if (it->second.m_result_type == MempoolAcceptResult::ResultType::VALID ||
|
||||
it->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY) {
|
||||
const auto& tx_result = it->second;
|
||||
switch(it->second.m_result_type) {
|
||||
case MempoolAcceptResult::ResultType::DIFFERENT_WITNESS:
|
||||
result_inner.pushKV("other-wtxid", it->second.m_other_wtxid.value().GetHex());
|
||||
break;
|
||||
case MempoolAcceptResult::ResultType::INVALID:
|
||||
result_inner.pushKV("error", it->second.m_state.ToString());
|
||||
break;
|
||||
case MempoolAcceptResult::ResultType::VALID:
|
||||
case MempoolAcceptResult::ResultType::MEMPOOL_ENTRY:
|
||||
result_inner.pushKV("vsize", int64_t{it->second.m_vsize.value()});
|
||||
UniValue fees(UniValue::VOBJ);
|
||||
fees.pushKV("base", ValueFromAmount(it->second.m_base_fees.value()));
|
||||
@ -959,6 +981,7 @@ static RPCHelpMan submitpackage()
|
||||
replaced_txids.insert(ptx->GetHash());
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
tx_result_map.pushKV(tx->GetWitnessHash().GetHex(), result_inner);
|
||||
}
|
||||
|
@ -125,8 +125,9 @@ class MempoolLimitTest(BitcoinTestFramework):
|
||||
utxo_to_spend=tx_B["new_utxo"],
|
||||
confirmed_only=True
|
||||
)
|
||||
|
||||
assert_raises_rpc_error(-26, "too-long-mempool-chain", node.submitpackage, [tx_B["hex"], tx_C["hex"]])
|
||||
res = node.submitpackage([tx_B["hex"], tx_C["hex"]])
|
||||
assert_equal(res["package_msg"], "transaction failed")
|
||||
assert "too-long-mempool-chain" in res["tx-results"][tx_C["wtxid"]]["error"]
|
||||
|
||||
def test_mid_package_eviction(self):
|
||||
node = self.nodes[0]
|
||||
@ -205,7 +206,7 @@ class MempoolLimitTest(BitcoinTestFramework):
|
||||
|
||||
# Package should be submitted, temporarily exceeding maxmempool, and then evicted.
|
||||
with node.assert_debug_log(expected_msgs=["rolling minimum fee bumped"]):
|
||||
assert_raises_rpc_error(-26, "mempool full", node.submitpackage, package_hex)
|
||||
assert_equal(node.submitpackage(package_hex)["package_msg"], "transaction failed")
|
||||
|
||||
# Maximum size must never be exceeded.
|
||||
assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
|
||||
@ -273,7 +274,9 @@ class MempoolLimitTest(BitcoinTestFramework):
|
||||
package_hex = [cpfp_parent["hex"], replacement_tx["hex"], child["hex"]]
|
||||
|
||||
# Package should be submitted, temporarily exceeding maxmempool, and then evicted.
|
||||
assert_raises_rpc_error(-26, "bad-txns-inputs-missingorspent", node.submitpackage, package_hex)
|
||||
res = node.submitpackage(package_hex)
|
||||
assert_equal(res["package_msg"], "transaction failed")
|
||||
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"])
|
||||
|
||||
# Maximum size must never be exceeded.
|
||||
assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
|
||||
@ -321,6 +324,7 @@ class MempoolLimitTest(BitcoinTestFramework):
|
||||
package_txns.append(tx_child)
|
||||
|
||||
submitpackage_result = node.submitpackage([tx["hex"] for tx in package_txns])
|
||||
assert_equal(submitpackage_result["package_msg"], "success")
|
||||
|
||||
rich_parent_result = submitpackage_result["tx-results"][tx_rich["wtxid"]]
|
||||
poor_parent_result = submitpackage_result["tx-results"][tx_poor["wtxid"]]
|
||||
@ -366,7 +370,9 @@ class MempoolLimitTest(BitcoinTestFramework):
|
||||
assert_greater_than(worst_feerate_btcvb, (parent_fee + child_fee) / (tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()))
|
||||
assert_greater_than(mempoolmin_feerate, (parent_fee) / (tx_parent_just_below["tx"].get_vsize()))
|
||||
assert_greater_than((parent_fee + child_fee) / (tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()), mempoolmin_feerate / 1000)
|
||||
assert_raises_rpc_error(-26, "mempool full", node.submitpackage, [tx_parent_just_below["hex"], tx_child_just_above["hex"]])
|
||||
res = node.submitpackage([tx_parent_just_below["hex"], tx_child_just_above["hex"]])
|
||||
for wtxid in [tx_parent_just_below["wtxid"], tx_child_just_above["wtxid"]]:
|
||||
assert_equal(res["tx-results"][wtxid]["error"], "mempool full")
|
||||
|
||||
self.log.info('Test passing a value below the minimum (5 MB) to -maxmempool throws an error')
|
||||
self.stop_node(0)
|
||||
|
@ -34,7 +34,6 @@ from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_greater_than,
|
||||
assert_greater_than_or_equal,
|
||||
assert_raises_rpc_error,
|
||||
)
|
||||
from test_framework.wallet import MiniWallet
|
||||
from test_framework.wallet_util import generate_keypair
|
||||
@ -169,7 +168,8 @@ class BytesPerSigOpTest(BitcoinTestFramework):
|
||||
assert_equal([x["package-error"] for x in packet_test], ["package-mempool-limits", "package-mempool-limits"])
|
||||
|
||||
# When we actually try to submit, the parent makes it into the mempool, but the child would exceed ancestor vsize limits
|
||||
assert_raises_rpc_error(-26, "too-long-mempool-chain", self.nodes[0].submitpackage, [tx_parent.serialize().hex(), tx_child.serialize().hex()])
|
||||
res = self.nodes[0].submitpackage([tx_parent.serialize().hex(), tx_child.serialize().hex()])
|
||||
assert "too-long-mempool-chain" in res["tx-results"][tx_child.getwtxid()]["error"]
|
||||
assert tx_parent.rehash() in self.nodes[0].getrawmempool()
|
||||
|
||||
# Transactions are tiny in weight
|
||||
|
@ -304,6 +304,7 @@ class RPCPackagesTest(BitcoinTestFramework):
|
||||
submitpackage_result = node.submitpackage(package=[tx["hex"] for tx in package_txns])
|
||||
|
||||
# Check that each result is present, with the correct size and fees
|
||||
assert_equal(submitpackage_result["package_msg"], "success")
|
||||
for package_txn in package_txns:
|
||||
tx = package_txn["tx"]
|
||||
assert tx.getwtxid() in submitpackage_result["tx-results"]
|
||||
@ -334,9 +335,26 @@ class RPCPackagesTest(BitcoinTestFramework):
|
||||
|
||||
self.log.info("Submitpackage only allows packages of 1 child with its parents")
|
||||
# Chain of 3 transactions has too many generations
|
||||
legacy_pool = node.getrawmempool()
|
||||
chain_hex = [t["hex"] for t in self.wallet.create_self_transfer_chain(chain_length=25)]
|
||||
assert_raises_rpc_error(-25, "package topology disallowed", node.submitpackage, chain_hex)
|
||||
assert_equal(legacy_pool, node.getrawmempool())
|
||||
|
||||
# Create a transaction chain such as only the parent gets accepted (by making the child's
|
||||
# version non-standard). Make sure the parent does get broadcast.
|
||||
self.log.info("If a package is partially submitted, transactions included in mempool get broadcast")
|
||||
peer = node.add_p2p_connection(P2PTxInvStore())
|
||||
txs = self.wallet.create_self_transfer_chain(chain_length=2)
|
||||
bad_child = tx_from_hex(txs[1]["hex"])
|
||||
bad_child.nVersion = -1
|
||||
hex_partial_acceptance = [txs[0]["hex"], bad_child.serialize().hex()]
|
||||
res = node.submitpackage(hex_partial_acceptance)
|
||||
assert_equal(res["package_msg"], "transaction failed")
|
||||
first_wtxid = txs[0]["tx"].getwtxid()
|
||||
assert "error" not in res["tx-results"][first_wtxid]
|
||||
sec_wtxid = bad_child.getwtxid()
|
||||
assert_equal(res["tx-results"][sec_wtxid]["error"], "version")
|
||||
peer.wait_for_broadcast([first_wtxid])
|
||||
|
||||
if __name__ == "__main__":
|
||||
RPCPackagesTest().main()
|
||||
|
Loading…
Reference in New Issue
Block a user