Skip to content

Commit 6a7d4b3

Browse files
achow101claude
authored andcommitted
Merge bitcoin#27609: rpc: allow submitpackage to be called outside of regtest
5b878be [doc] add release note for submitpackage (glozow) 7a9bb2a [rpc] allow submitpackage to be called outside of regtest (glozow) 5b9087a [rpc] require package to be a tree in submitpackage (glozow) e32ba15 [txpackages] IsChildWithParentsTree() (glozow) b4f28cc [doc] parent pay for child in aggregate CheckFeeRate (glozow) Pull request description: Permit (restricted topology) submitpackage RPC outside of regtest. Suggested in bitcoin#26933 (comment) This RPC should be safe but still experimental - interface may change, not all features (e.g. package RBF) are implemented, etc. If a miner wants to expose this to people, they can effectively use "package relay" before the p2p changes are implemented. However, please note **this is not package relay**; transactions submitted this way will not relay to other nodes if the feerates are below their mempool min fee. Users should put this behind some kind of rate limit or permissions. ACKs for top commit: instagibbs: ACK 5b878be achow101: ACK 5b878be dergoegge: Code review ACK 5b878be ajtowns: ACK 5b878be ariard: Code Review ACK 5b878be. Though didn’t manually test the PR. Tree-SHA512: 610365c0b2ffcccd55dedd1151879c82de1027e3319712bcb11d54f2467afaae4d05dca5f4b25f03354c80845fef538d3938b958174dda8b14c10670537a6524
1 parent dc63e25 commit 6a7d4b3

7 files changed

Lines changed: 329 additions & 0 deletions

File tree

doc/release-notes-27609.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
- A new RPC, `submitpackage`, has been added. It can be used to submit a list of raw hex
2+
transactions to the mempool to be evaluated as a package using consensus and mempool policy rules.
3+
These policies include package CPFP, allowing a child with high fees to bump a parent below the
4+
mempool minimum feerate (but not minimum relay feerate).
5+
6+
- Warning: successful submission does not mean the transactions will propagate throughout the
7+
network, as package relay is not supported.
8+
9+
- Not all features are available. The package is limited to a child with all of its
10+
unconfirmed parents, and no parent may spend the output of another parent. Also, package
11+
RBF is not supported. Refer to doc/policy/packages.md for more details on package policies
12+
and limitations.
13+
14+
- This RPC is experimental. Its interface may change.

src/policy/packages.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,18 @@ bool IsChildWithParents(const Package& package)
8181
return std::all_of(package.cbegin(), package.cend() - 1,
8282
[&input_txids](const auto& ptx) { return input_txids.count(ptx->GetHash()) > 0; });
8383
}
84+
85+
bool IsChildWithParentsTree(const Package& package)
86+
{
87+
if (!IsChildWithParents(package)) return false;
88+
std::unordered_set<uint256, SaltedTxidHasher> parent_txids;
89+
std::transform(package.cbegin(), package.cend() - 1, std::inserter(parent_txids, parent_txids.end()),
90+
[](const auto& ptx) { return ptx->GetHash(); });
91+
// Each parent must not have an input who is one of the other parents.
92+
return std::all_of(package.cbegin(), package.cend() - 1, [&](const auto& ptx) {
93+
for (const auto& input : ptx->vin) {
94+
if (parent_txids.count(input.prevout.hash) > 0) return false;
95+
}
96+
return true;
97+
});
98+
}

src/policy/packages.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,8 @@ bool CheckPackage(const Package& txns, PackageValidationState& state);
5959
*/
6060
bool IsChildWithParents(const Package& package);
6161

62+
/** Context-free check that a package IsChildWithParents() and none of the parents depend on each
63+
* other (the package is a "tree").
64+
*/
65+
bool IsChildWithParentsTree(const Package& package);
6266
#endif // BITCOIN_POLICY_PACKAGES_H

src/rpc/mempool.cpp

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,160 @@ RPCHelpMan savemempool()
468468
};
469469
}
470470

471+
static RPCHelpMan submitpackage()
472+
{
473+
return RPCHelpMan{"submitpackage",
474+
"Submit a package of raw transactions (serialized, hex-encoded) to local node.\n"
475+
"The package must consist of a child with its parents, and none of the parents may depend on one another.\n"
476+
"The package will be validated according to consensus and mempool policy rules. If all transactions pass, they will be accepted to mempool.\n"
477+
"This RPC is experimental and the interface may be unstable. Refer to doc/policy/packages.md for documentation on package policies.\n"
478+
"Warning: successful submission does not mean the transactions will propagate throughout the network.\n"
479+
,
480+
{
481+
{"package", RPCArg::Type::ARR, RPCArg::Optional::NO, "An array of raw transactions.",
482+
{
483+
{"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""},
484+
},
485+
},
486+
},
487+
RPCResult{
488+
RPCResult::Type::OBJ, "", "",
489+
{
490+
{RPCResult::Type::OBJ_DYN, "tx-results", "transaction results keyed by wtxid",
491+
{
492+
{RPCResult::Type::OBJ, "wtxid", "transaction wtxid", {
493+
{RPCResult::Type::STR_HEX, "txid", "The transaction hash in hex"},
494+
{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."},
495+
{RPCResult::Type::NUM, "vsize", "Virtual transaction size as defined in BIP 141."},
496+
{RPCResult::Type::OBJ, "fees", "Transaction fees", {
497+
{RPCResult::Type::STR_AMOUNT, "base", "transaction fee in " + CURRENCY_UNIT},
498+
{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."},
499+
{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.",
500+
{{RPCResult::Type::STR_HEX, "", "transaction wtxid in hex"},
501+
}},
502+
}},
503+
}}
504+
}},
505+
{RPCResult::Type::ARR, "replaced-transactions", /*optional=*/true, "List of txids of replaced transactions",
506+
{
507+
{RPCResult::Type::STR_HEX, "", "The transaction id"},
508+
}},
509+
},
510+
},
511+
RPCExamples{
512+
HelpExampleCli("testmempoolaccept", "[rawtx1, rawtx2]") +
513+
HelpExampleCli("submitpackage", "[rawtx1, rawtx2]")
514+
},
515+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
516+
{
517+
const UniValue raw_transactions = request.params[0].get_array();
518+
if (raw_transactions.size() < 1 || raw_transactions.size() > MAX_PACKAGE_COUNT) {
519+
throw JSONRPCError(RPC_INVALID_PARAMETER,
520+
"Array must contain between 1 and " + ToString(MAX_PACKAGE_COUNT) + " transactions.");
521+
}
522+
523+
std::vector<CTransactionRef> txns;
524+
txns.reserve(raw_transactions.size());
525+
for (const auto& rawtx : raw_transactions.getValues()) {
526+
CMutableTransaction mtx;
527+
if (!DecodeHexTx(mtx, rawtx.get_str())) {
528+
throw JSONRPCError(RPC_DESERIALIZATION_ERROR,
529+
"TX decode failed: " + rawtx.get_str() + " Make sure the tx has at least one input.");
530+
}
531+
txns.emplace_back(MakeTransactionRef(std::move(mtx)));
532+
}
533+
if (!IsChildWithParentsTree(txns)) {
534+
throw JSONRPCTransactionError(TransactionError::INVALID_PACKAGE, "package topology disallowed. not child-with-parents or parents depend on each other.");
535+
}
536+
537+
NodeContext& node = EnsureAnyNodeContext(request.context);
538+
CTxMemPool& mempool = EnsureMemPool(node);
539+
Chainstate& chainstate = EnsureChainman(node).ActiveChainstate();
540+
const auto package_result = WITH_LOCK(::cs_main, return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/ false));
541+
542+
// First catch any errors.
543+
switch(package_result.m_state.GetResult()) {
544+
case PackageValidationResult::PCKG_RESULT_UNSET: break;
545+
case PackageValidationResult::PCKG_POLICY:
546+
{
547+
throw JSONRPCTransactionError(TransactionError::INVALID_PACKAGE,
548+
package_result.m_state.GetRejectReason());
549+
}
550+
case PackageValidationResult::PCKG_MEMPOOL_ERROR:
551+
{
552+
throw JSONRPCTransactionError(TransactionError::MEMPOOL_ERROR,
553+
package_result.m_state.GetRejectReason());
554+
}
555+
case PackageValidationResult::PCKG_TX:
556+
{
557+
for (const auto& tx : txns) {
558+
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
559+
if (it != package_result.m_tx_results.end() && it->second.m_state.IsInvalid()) {
560+
throw JSONRPCTransactionError(TransactionError::MEMPOOL_REJECTED,
561+
strprintf("%s failed: %s", tx->GetHash().ToString(), it->second.m_state.GetRejectReason()));
562+
}
563+
}
564+
// If a PCKG_TX error was returned, there must have been an invalid transaction.
565+
NONFATAL_UNREACHABLE();
566+
}
567+
}
568+
size_t num_broadcast{0};
569+
for (const auto& tx : txns) {
570+
std::string err_string;
571+
const auto err = BroadcastTransaction(node, tx, err_string, /*max_tx_fee=*/0, /*relay=*/true, /*wait_callback=*/true);
572+
if (err != TransactionError::OK) {
573+
throw JSONRPCTransactionError(err,
574+
strprintf("transaction broadcast failed: %s (all transactions were submitted, %d transactions were broadcast successfully)",
575+
err_string, num_broadcast));
576+
}
577+
num_broadcast++;
578+
}
579+
UniValue rpc_result{UniValue::VOBJ};
580+
UniValue tx_result_map{UniValue::VOBJ};
581+
std::set<uint256> replaced_txids;
582+
for (const auto& tx : txns) {
583+
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
584+
CHECK_NONFATAL(it != package_result.m_tx_results.end());
585+
UniValue result_inner{UniValue::VOBJ};
586+
result_inner.pushKV("txid", tx->GetHash().GetHex());
587+
const auto& tx_result = it->second;
588+
if (it->second.m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS) {
589+
result_inner.pushKV("other-wtxid", it->second.m_other_wtxid.value().GetHex());
590+
}
591+
if (it->second.m_result_type == MempoolAcceptResult::ResultType::VALID ||
592+
it->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY) {
593+
result_inner.pushKV("vsize", int64_t{it->second.m_vsize.value()});
594+
UniValue fees(UniValue::VOBJ);
595+
fees.pushKV("base", ValueFromAmount(it->second.m_base_fees.value()));
596+
if (tx_result.m_result_type == MempoolAcceptResult::ResultType::VALID) {
597+
// Effective feerate is not provided for MEMPOOL_ENTRY transactions even
598+
// though modified fees is known, because it is unknown whether package
599+
// feerate was used when it was originally submitted.
600+
fees.pushKV("effective-feerate", ValueFromAmount(tx_result.m_effective_feerate.value().GetFeePerK()));
601+
UniValue effective_includes_res(UniValue::VARR);
602+
for (const auto& wtxid : tx_result.m_wtxids_fee_calculations.value()) {
603+
effective_includes_res.push_back(wtxid.ToString());
604+
}
605+
fees.pushKV("effective-includes", effective_includes_res);
606+
}
607+
result_inner.pushKV("fees", fees);
608+
if (it->second.m_replaced_transactions.has_value()) {
609+
for (const auto& ptx : it->second.m_replaced_transactions.value()) {
610+
replaced_txids.insert(ptx->GetHash());
611+
}
612+
}
613+
}
614+
tx_result_map.pushKV(tx->GetWitnessHash().GetHex(), result_inner);
615+
}
616+
rpc_result.pushKV("tx-results", tx_result_map);
617+
UniValue replaced_list(UniValue::VARR);
618+
for (const uint256& hash : replaced_txids) replaced_list.push_back(hash.ToString());
619+
rpc_result.pushKV("replaced-transactions", replaced_list);
620+
return rpc_result;
621+
},
622+
};
623+
}
624+
471625
void RegisterMempoolRPCCommands(CRPCTable& t)
472626
{
473627
static const CRPCCommand commands[]{
@@ -479,6 +633,7 @@ void RegisterMempoolRPCCommands(CRPCTable& t)
479633
{"blockchain", &getmempoolinfo},
480634
{"blockchain", &getrawmempool},
481635
{"blockchain", &savemempool},
636+
{"rawtransactions", &submitpackage},
482637
};
483638
for (const auto& c : commands) {
484639
t.appendCommand(c.name, &c);

src/test/txpackage_tests.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ BOOST_FIXTURE_TEST_CASE(noncontextual_package_tests, TestChain100NoDIP0001Setup)
145145
BOOST_CHECK_EQUAL(state.GetResult(), PackageValidationResult::PCKG_POLICY);
146146
BOOST_CHECK_EQUAL(state.GetRejectReason(), "package-not-sorted");
147147
BOOST_CHECK(IsChildWithParents({tx_parent, tx_child}));
148+
BOOST_CHECK(IsChildWithParentsTree({tx_parent, tx_child}));
148149
}
149150

150151
// 24 Parents and 1 Child
@@ -170,6 +171,7 @@ BOOST_FIXTURE_TEST_CASE(noncontextual_package_tests, TestChain100NoDIP0001Setup)
170171
PackageValidationState state;
171172
BOOST_CHECK(CheckPackage(package, state));
172173
BOOST_CHECK(IsChildWithParents(package));
174+
BOOST_CHECK(IsChildWithParentsTree(package));
173175

174176
package.erase(package.begin());
175177
BOOST_CHECK(IsChildWithParents(package));
@@ -202,6 +204,7 @@ BOOST_FIXTURE_TEST_CASE(noncontextual_package_tests, TestChain100NoDIP0001Setup)
202204
BOOST_CHECK(IsChildWithParents({tx_parent, tx_parent_also_child}));
203205
BOOST_CHECK(IsChildWithParents({tx_parent, tx_child}));
204206
BOOST_CHECK(IsChildWithParents({tx_parent, tx_parent_also_child, tx_child}));
207+
BOOST_CHECK(!IsChildWithParentsTree({tx_parent, tx_parent_also_child, tx_child}));
205208
// IsChildWithParents does not detect unsorted parents.
206209
BOOST_CHECK(IsChildWithParents({tx_parent_also_child, tx_parent, tx_child}));
207210
BOOST_CHECK(CheckPackage({tx_parent, tx_parent_also_child, tx_child}, state));

src/validation.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,27 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
11971197
m_viewmempool.PackageAddTransaction(ws.m_ptx);
11981198
}
11991199

1200+
// Transactions must meet two minimum feerates: the mempool minimum fee and min relay fee.
1201+
// For transactions consisting of exactly one child and its parents, it suffices to use the
1202+
// package feerate (total modified fees / total virtual size) to check this requirement.
1203+
// Note that this is an aggregate feerate; this function has not checked that there are transactions
1204+
// too low feerate to pay for themselves, or that the child transactions are higher feerate than
1205+
// their parents. Using aggregate feerate may allow "parents pay for child" behavior and permit
1206+
// a child that is below mempool minimum feerate. To avoid these behaviors, callers of
1207+
// AcceptMultipleTransactions need to restrict txns topology (e.g. to ancestor sets) and check
1208+
// the feerates of individuals and subsets.
1209+
const auto m_total_vsize = std::accumulate(workspaces.cbegin(), workspaces.cend(), int64_t{0},
1210+
[](int64_t sum, auto& ws) { return sum + ws.m_vsize; });
1211+
const auto m_total_modified_fees = std::accumulate(workspaces.cbegin(), workspaces.cend(), CAmount{0},
1212+
[](CAmount sum, auto& ws) { return sum + ws.m_modified_fees; });
1213+
const CFeeRate package_feerate(m_total_modified_fees, m_total_vsize);
1214+
TxValidationState placeholder_state;
1215+
if (args.m_package_feerates &&
1216+
!CheckFeeRate(m_total_vsize, m_total_modified_fees, placeholder_state)) {
1217+
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-fee-too-low");
1218+
return PackageMempoolAcceptResult(package_state, {});
1219+
}
1220+
12001221
// Apply package mempool ancestor/descendant limits. Skip if there is only one transaction,
12011222
// because it's unnecessary. Also, CPFP carve out can increase the limit for individual
12021223
// transactions, but this exemption is not extended to packages in CheckPackageLimits().

0 commit comments

Comments
 (0)