diff --git a/src/init.cpp b/src/init.cpp index 7b6b188c257d..e03173fffb9d 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -759,6 +759,10 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) strprintf("Refuse to relay or mine transactions involving non-bitcoin tokens (default: %u)", DEFAULT_REJECT_TOKENS), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); + argsman.AddArg("-rejectnetnegativeinputs", + strprintf("Reject transactions containing inputs whose spending cost (at the transaction's feerate) exceeds the value of the input (default: %u)", + DEFAULT_REJECT_NET_NEGATIVE_INPUTS), + ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-spkreuse=", strprintf("Either \"allow\" to relay/mine transactions reusing addresses or other pubkey scripts, or \"conflict\" to treat them as exclusive prior to being mined (default: %s)", DEFAULT_SPKREUSE), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-whitelistforcerelay", strprintf("Add 'forcerelay' permission to whitelisted peers with default permissions. This will relay transactions even if the transactions were already in the mempool. (default: %d)", DEFAULT_WHITELISTFORCERELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-whitelistrelay", strprintf("Add 'relay' permission to whitelisted peers with default permissions. This will accept relayed transactions even when not relaying transactions (default: %d)", DEFAULT_WHITELISTRELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); diff --git a/src/kernel/mempool_options.h b/src/kernel/mempool_options.h index f6a37e958c0b..5fbdd0171c38 100644 --- a/src/kernel/mempool_options.h +++ b/src/kernel/mempool_options.h @@ -86,6 +86,7 @@ struct MemPoolOptions { bool permit_bare_multisig{DEFAULT_PERMIT_BAREMULTISIG}; bool reject_parasites{DEFAULT_REJECT_PARASITES}; bool reject_tokens{DEFAULT_REJECT_TOKENS}; + bool reject_net_negative_inputs{DEFAULT_REJECT_NET_NEGATIVE_INPUTS}; bool accept_non_std_datacarrier{DEFAULT_ACCEPT_NON_STD_DATACARRIER}; bool require_standard{true}; bool acceptunknownwitness{DEFAULT_ACCEPTUNKNOWNWITNESS}; diff --git a/src/node/mempool_args.cpp b/src/node/mempool_args.cpp index 1d82d8f4426f..045e70286efd 100644 --- a/src/node/mempool_args.cpp +++ b/src/node/mempool_args.cpp @@ -205,6 +205,8 @@ util::Result ApplyArgsManOptions(const ArgsManager& argsman, const CChainP mempool_opts.reject_tokens = argsman.GetBoolArg("-rejecttokens", DEFAULT_REJECT_TOKENS); + mempool_opts.reject_net_negative_inputs = argsman.GetBoolArg("-rejectnetnegativeinputs", DEFAULT_REJECT_NET_NEGATIVE_INPUTS); + if (argsman.GetBoolArg("-datacarrier", DEFAULT_ACCEPT_DATACARRIER)) { mempool_opts.max_datacarrier_bytes = argsman.GetIntArg("-datacarriersize", MAX_OP_RETURN_RELAY); } else { diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index 589ba4c8c7e9..e4e2aa567811 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -287,7 +287,7 @@ static bool CheckSigopsBIP54(const CTransaction& tx, const CCoinsViewCache& inpu * * We also check the total number of non-witness sigops across the whole transaction, as per BIP54. */ -bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, const kernel::MemPoolOptions& opts, const std::string& reason_prefix, std::string& out_reason, const ignore_rejects_type& ignore_rejects) +bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, const kernel::MemPoolOptions& opts, const CFeeRate& tx_feerate, const std::string& reason_prefix, std::string& out_reason, const ignore_rejects_type& ignore_rejects) { if (tx.IsCoinBase()) { return true; // Coinbases don't use vin normally @@ -343,6 +343,17 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, MaybeReject("scriptcheck-sigops"); } } + + if (opts.reject_net_negative_inputs && whichType == TxoutType::WITNESS_V1_TAPROOT) { + const auto& stack = tx.vin[i].scriptWitness.stack; + if (stack.size() >= 2) { + int32_t input_vsize = (GetTransactionInputWeight(tx.vin[i]) + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR; + CAmount input_cost = tx_feerate.GetFee(input_vsize); + if (input_cost > prev.nValue) { + MaybeReject("netnegative"); + } + } + } } return true; diff --git a/src/policy/policy.h b/src/policy/policy.h index 9dbab66a75e0..716f1c5195f5 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -66,6 +66,8 @@ static constexpr unsigned int DEFAULT_BYTES_PER_SIGOP_STRICT{20}; static constexpr unsigned int DEFAULT_WEIGHT_PER_DATA_BYTE{4}; /** Default for -rejecttokens */ static constexpr bool DEFAULT_REJECT_TOKENS{false}; +/** Default for -rejectnetnegativeinputs */ +static constexpr bool DEFAULT_REJECT_NET_NEGATIVE_INPUTS{false}; // NOTE: Changes to these three require manually adjusting doc in init.cpp /** Default for -permitephemeral=send */ @@ -205,7 +207,7 @@ bool IsStandardTx(const CTransaction& tx, const kernel::MemPoolOptions& opts, st * @param[in] mapInputs Map of previous transactions that have outputs we're spending * @return True if all inputs (scriptSigs) use only standard transaction forms */ -bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, const kernel::MemPoolOptions& opts, const std::string& reason_prefix, std::string& out_reason, const ignore_rejects_type& ignore_rejects=empty_ignore_rejects); +bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, const kernel::MemPoolOptions& opts, const CFeeRate& tx_feerate, const std::string& reason_prefix, std::string& out_reason, const ignore_rejects_type& ignore_rejects=empty_ignore_rejects); /** * Check if the transaction is over standard P2WSH resources limit: diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 21fef145e3b5..d465c8f3087e 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -412,6 +412,12 @@ OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet) verticalLayout_Spamfiltering->addWidget(rejecttokens); FixTabOrder(rejecttokens); + rejectnetnegativeinputs = new QCheckBox(groupBox_Spamfiltering); + rejectnetnegativeinputs->setText(tr("Reject overly net-negative value inputs")); + rejectnetnegativeinputs->setToolTip(tr("Reject transactions containing inputs whose spending cost (at the transaction's feerate) exceeds the value of the input being spent. Key-path spends are always allowed.")); + verticalLayout_Spamfiltering->addWidget(rejectnetnegativeinputs); + FixTabOrder(rejectnetnegativeinputs); + minrelaytxfee = new BitcoinAmountField(groupBox_Spamfiltering); CreateOptionUI(verticalLayout_Spamfiltering, minrelaytxfee, tr("Ignore transactions offering miners less than %s per kvB in transaction fees.")); @@ -933,6 +939,7 @@ void OptionsDialog::setMapper() mapper->addMapping(rejectunknownwitness, OptionsModel::rejectunknownwitness); mapper->addMapping(rejectparasites, OptionsModel::rejectparasites); mapper->addMapping(rejecttokens, OptionsModel::rejecttokens); + mapper->addMapping(rejectnetnegativeinputs, OptionsModel::rejectnetnegativeinputs); mapper->addMapping(rejectspkreuse, OptionsModel::rejectspkreuse); mapper->addMapping(minrelaytxfee, OptionsModel::minrelaytxfee); mapper->addMapping(minrelaycoinblocks, OptionsModel::minrelaycoinblocks); diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index f64603fa364a..4a356d94d80d 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -138,6 +138,7 @@ private Q_SLOTS: QCheckBox *rejectunknownwitness; QCheckBox *rejectparasites; QCheckBox *rejecttokens; + QCheckBox *rejectnetnegativeinputs; QCheckBox *rejectspkreuse; BitcoinAmountField *minrelaytxfee; BitcoinAmountField *minrelaycoinblocks; diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 5afd127ddd4e..540e64ff2b41 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -90,6 +90,7 @@ static const char* SettingName(OptionsModel::OptionID option) case OptionsModel::rejectunknownwitness: return "rejectunknownwitness"; case OptionsModel::rejectparasites: return "rejectparasites"; case OptionsModel::rejecttokens: return "rejecttokens"; + case OptionsModel::rejectnetnegativeinputs: return "rejectnetnegativeinputs"; case OptionsModel::rejectspkreuse: return "rejectspkreuse"; case OptionsModel::minrelaytxfee: return "minrelaytxfee"; case OptionsModel::minrelaycoinblocks: return "minrelaycoinblocks"; @@ -729,6 +730,8 @@ QVariant OptionsModel::getOption(OptionID option, const std::string& suffix) con return node().mempool().m_opts.reject_parasites; case rejecttokens: return node().mempool().m_opts.reject_tokens; + case rejectnetnegativeinputs: + return node().mempool().m_opts.reject_net_negative_inputs; case rejectspkreuse: return f_rejectspkreuse; case minrelaytxfee: @@ -1249,6 +1252,15 @@ bool OptionsModel::setOption(OptionID option, const QVariant& value, const std:: } break; } + case rejectnetnegativeinputs: + { + if (changed()) { + const bool nv = value.toBool(); + node().mempool().m_opts.reject_net_negative_inputs = nv; + node().updateRwSetting("rejectnetnegativeinputs", nv); + } + break; + } case rejectspkreuse: if (changed()) { const bool fNewValue = value.toBool(); diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index c4531d6e70e8..299b402ab431 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -91,6 +91,7 @@ class OptionsModel : public QAbstractListModel rejectunknownwitness, // bool rejectparasites, // bool rejecttokens, // bool + rejectnetnegativeinputs, // bool rejectspkreuse, // bool minrelaytxfee, minrelaycoinblocks, diff --git a/src/test/util/transaction_utils.h b/src/test/util/transaction_utils.h index 5e4ea161076f..388620ba296d 100644 --- a/src/test/util/transaction_utils.h +++ b/src/test/util/transaction_utils.h @@ -6,6 +6,7 @@ #define BITCOIN_TEST_UTIL_TRANSACTION_UTILS_H #include +#include #include #include #include