Skip to content

Commit 38f70ba

Browse files
committed
RPC: Add maxfeerate and maxburnamount args to submitpackage
And thread the feerate value through ProcessNewPackage to reject individual transactions that exceed the given feerate. This allows subpackage processing, and is compatible with future package RBF work.
1 parent 45b2a91 commit 38f70ba

File tree

9 files changed

+124
-30
lines changed

9 files changed

+124
-30
lines changed

src/node/transaction.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ struct NodeContext;
2626
*/
2727
static const CFeeRate DEFAULT_MAX_RAW_TX_FEE_RATE{COIN / 10};
2828

29+
/** Maximum burn value for sendrawtransaction, submitpackage, and testmempoolaccept RPC calls.
30+
* By default, a transaction with a burn value higher than this will be rejected
31+
* by these RPCs and the GUI. This can be overridden with the maxburnamount argument.
32+
*/
33+
static const CAmount DEFAULT_MAX_BURN_AMOUNT{0};
34+
2935
/**
3036
* Submit a transaction to the mempool and (optionally) relay it to all P2P peers.
3137
*

src/rpc/client.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
128128
{ "testmempoolaccept", 0, "rawtxs" },
129129
{ "testmempoolaccept", 1, "maxfeerate" },
130130
{ "submitpackage", 0, "package" },
131+
{ "submitpackage", 1, "maxfeerate" },
132+
{ "submitpackage", 2, "maxburnamount" },
131133
{ "combinerawtransaction", 0, "txs" },
132134
{ "fundrawtransaction", 1, "options" },
133135
{ "fundrawtransaction", 1, "add_inputs"},

src/rpc/mempool.cpp

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
using kernel::DumpMempool;
3030

31+
using node::DEFAULT_MAX_BURN_AMOUNT;
3132
using node::DEFAULT_MAX_RAW_TX_FEE_RATE;
3233
using node::MempoolPath;
3334
using node::NodeContext;
@@ -46,7 +47,7 @@ static RPCHelpMan sendrawtransaction()
4647
{"maxfeerate", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK())},
4748
"Reject transactions whose fee rate is higher than the specified value, expressed in " + CURRENCY_UNIT +
4849
"/kvB.\nFee rates larger than 1BTC/kvB are rejected.\nSet to 0 to accept any fee rate."},
49-
{"maxburnamount", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(0)},
50+
{"maxburnamount", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(DEFAULT_MAX_BURN_AMOUNT)},
5051
"Reject transactions with provably unspendable outputs (e.g. 'datacarrier' outputs that use the OP_RETURN opcode) greater than the specified value, expressed in " + CURRENCY_UNIT + ".\n"
5152
"If burning funds through unspendable outputs is desired, increase this value.\n"
5253
"This check is based on heuristics and does not guarantee spendability of outputs.\n"},
@@ -180,7 +181,7 @@ static RPCHelpMan testmempoolaccept()
180181
Chainstate& chainstate = chainman.ActiveChainstate();
181182
const PackageMempoolAcceptResult package_result = [&] {
182183
LOCK(::cs_main);
183-
if (txns.size() > 1) return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/true);
184+
if (txns.size() > 1) return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/true, /*max_sane_feerate=*/{});
184185
return PackageMempoolAcceptResult(txns[0]->GetWitnessHash(),
185186
chainman.ProcessTransaction(txns[0], /*test_accept=*/true));
186187
}();
@@ -823,6 +824,14 @@ static RPCHelpMan submitpackage()
823824
{"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""},
824825
},
825826
},
827+
{"maxfeerate", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK())},
828+
"Reject transactions whose fee rate is higher than the specified value, expressed in " + CURRENCY_UNIT +
829+
"/kvB.\nFee rates larger than 1BTC/kvB are rejected.\nSet to 0 to accept any fee rate."},
830+
{"maxburnamount", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(DEFAULT_MAX_BURN_AMOUNT)},
831+
"Reject transactions with provably unspendable outputs (e.g. 'datacarrier' outputs that use the OP_RETURN opcode) greater than the specified value, expressed in " + CURRENCY_UNIT + ".\n"
832+
"If burning funds through unspendable outputs is desired, increase this value.\n"
833+
"This check is based on heuristics and does not guarantee spendability of outputs.\n"
834+
},
826835
},
827836
RPCResult{
828837
RPCResult::Type::OBJ, "", "",
@@ -862,6 +871,17 @@ static RPCHelpMan submitpackage()
862871
"Array must contain between 1 and " + ToString(MAX_PACKAGE_COUNT) + " transactions.");
863872
}
864873

874+
// Fee check needs to be run with chainstate and package context
875+
const CFeeRate max_raw_tx_fee_rate = ParseFeeRate(self.Arg<UniValue>(1));
876+
std::optional<CFeeRate> max_sane_feerate{max_raw_tx_fee_rate};
877+
// 0-value is special; it's mapped to no sanity check
878+
if (max_raw_tx_fee_rate == CFeeRate(0)) {
879+
max_sane_feerate = std::nullopt;
880+
}
881+
882+
// Burn sanity check is run with no context
883+
const CAmount max_burn_amount = request.params[2].isNull() ? 0 : AmountFromValue(request.params[2]);
884+
865885
std::vector<CTransactionRef> txns;
866886
txns.reserve(raw_transactions.size());
867887
for (const auto& rawtx : raw_transactions.getValues()) {
@@ -870,6 +890,13 @@ static RPCHelpMan submitpackage()
870890
throw JSONRPCError(RPC_DESERIALIZATION_ERROR,
871891
"TX decode failed: " + rawtx.get_str() + " Make sure the tx has at least one input.");
872892
}
893+
894+
for (const auto& out : mtx.vout) {
895+
if((out.scriptPubKey.IsUnspendable() || !out.scriptPubKey.HasValidOps()) && out.nValue > max_burn_amount) {
896+
throw JSONRPCTransactionError(TransactionError::MAX_BURN_EXCEEDED);
897+
}
898+
}
899+
873900
txns.emplace_back(MakeTransactionRef(std::move(mtx)));
874901
}
875902
if (!IsChildWithParentsTree(txns)) {
@@ -879,7 +906,7 @@ static RPCHelpMan submitpackage()
879906
NodeContext& node = EnsureAnyNodeContext(request.context);
880907
CTxMemPool& mempool = EnsureMemPool(node);
881908
Chainstate& chainstate = EnsureChainman(node).ActiveChainstate();
882-
const auto package_result = WITH_LOCK(::cs_main, return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/ false));
909+
const auto package_result = WITH_LOCK(::cs_main, return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/ false, max_sane_feerate));
883910

884911
std::string package_msg = "success";
885912

src/test/fuzz/package_eval.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
277277
auto single_submit = txs.size() == 1 && fuzzed_data_provider.ConsumeBool();
278278

279279
const auto result_package = WITH_LOCK(::cs_main,
280-
return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit));
280+
return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit, /*max_sane_feerate=*/{}));
281281

282282
// Always set bypass_limits to false because it is not supported in ProcessNewPackage and
283283
// can be a source of divergence.

src/test/fuzz/tx_pool.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
291291
// Make sure ProcessNewPackage on one transaction works.
292292
// The result is not guaranteed to be the same as what is returned by ATMP.
293293
const auto result_package = WITH_LOCK(::cs_main,
294-
return ProcessNewPackage(chainstate, tx_pool, {tx}, true));
294+
return ProcessNewPackage(chainstate, tx_pool, {tx}, true, /*max_sane_feerate=*/{}));
295295
// If something went wrong due to a package-specific policy, it might not return a
296296
// validation result for the transaction.
297297
if (result_package.m_state.GetResult() != PackageValidationResult::PCKG_POLICY) {

0 commit comments

Comments
 (0)