mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-07 14:25:09 -05:00
Merge 46f0bc5b48
into 85f96b01b7
This commit is contained in:
commit
8656665641
27 changed files with 161 additions and 57 deletions
7
doc/release-notes-29278.md
Normal file
7
doc/release-notes-29278.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
wallet startup option
|
||||
========================
|
||||
|
||||
- A new wallet startip option `-maxfeerate` is added.
|
||||
- This option sets the upper limit for wallet transaction fee rate.
|
||||
- The wallet will now refrain from creating transactions with a fee rate exceeding the `maxfeerate`.
|
||||
- The default fee rate is 10,000 satoshis per virtual byte.
|
|
@ -135,7 +135,9 @@ bilingual_str TransactionErrorString(const TransactionError err)
|
|||
case TransactionError::MEMPOOL_ERROR:
|
||||
return Untranslated("Mempool internal error");
|
||||
case TransactionError::MAX_FEE_EXCEEDED:
|
||||
return Untranslated("Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)");
|
||||
return Untranslated("Fee exceeds maximum configured by user (maxtxfee)");
|
||||
case TransactionError::MAX_FEE_RATE_EXCEEDED:
|
||||
return Untranslated("Fee rate exceeds maximum configured by user (maxfeerate)");
|
||||
case TransactionError::MAX_BURN_EXCEEDED:
|
||||
return Untranslated("Unspendable output exceeds maximum configured by user (maxburnamount)");
|
||||
case TransactionError::INVALID_PACKAGE:
|
||||
|
|
|
@ -37,6 +37,7 @@ void DummyWalletInit::AddWalletOptions(ArgsManager& argsman) const
|
|||
"-keypool=<n>",
|
||||
"-maxapsfee=<n>",
|
||||
"-maxtxfee=<amt>",
|
||||
"-maxfeerate=<amt>",
|
||||
"-mintxfee=<amt>",
|
||||
"-paytxfee=<amt>",
|
||||
"-signer=<cmd>",
|
||||
|
|
|
@ -217,6 +217,7 @@ public:
|
|||
//! Return false if the transaction could not be added due to the fee or for another reason.
|
||||
virtual bool broadcastTransaction(const CTransactionRef& tx,
|
||||
const CAmount& max_tx_fee,
|
||||
const CFeeRate& max_tx_fee_rate,
|
||||
bool relay,
|
||||
std::string& err_string) = 0;
|
||||
|
||||
|
|
|
@ -215,7 +215,7 @@ public:
|
|||
virtual std::optional<Coin> getUnspentOutput(const COutPoint& output) = 0;
|
||||
|
||||
//! Broadcast transaction.
|
||||
virtual node::TransactionError broadcastTransaction(CTransactionRef tx, CAmount max_tx_fee, std::string& err_string) = 0;
|
||||
virtual node::TransactionError broadcastTransaction(CTransactionRef tx, CAmount max_tx_fee, CFeeRate max_tx_fee_rate, std::string& err_string) = 0;
|
||||
|
||||
//! Get wallet loader.
|
||||
virtual WalletLoader& walletLoader() = 0;
|
||||
|
|
|
@ -361,9 +361,9 @@ public:
|
|||
LOCK(::cs_main);
|
||||
return chainman().ActiveChainstate().CoinsTip().GetCoin(output);
|
||||
}
|
||||
TransactionError broadcastTransaction(CTransactionRef tx, CAmount max_tx_fee, std::string& err_string) override
|
||||
TransactionError broadcastTransaction(CTransactionRef tx, CAmount max_tx_fee, CFeeRate max_tx_fee_rate, std::string& err_string) override
|
||||
{
|
||||
return BroadcastTransaction(*m_context, std::move(tx), err_string, max_tx_fee, /*relay=*/ true, /*wait_callback=*/ false);
|
||||
return BroadcastTransaction(*m_context, std::move(tx), err_string, max_tx_fee, max_tx_fee_rate, /*relay=*/true, /*wait_callback=*/false);
|
||||
}
|
||||
WalletLoader& walletLoader() override
|
||||
{
|
||||
|
@ -683,11 +683,12 @@ public:
|
|||
return entry->GetCountWithDescendants() > 1;
|
||||
}
|
||||
bool broadcastTransaction(const CTransactionRef& tx,
|
||||
const CAmount& max_tx_fee,
|
||||
bool relay,
|
||||
std::string& err_string) override
|
||||
const CAmount& max_tx_fee,
|
||||
const CFeeRate& max_tx_fee_rate,
|
||||
bool relay,
|
||||
std::string& err_string) override
|
||||
{
|
||||
const TransactionError err = BroadcastTransaction(m_node, tx, err_string, max_tx_fee, relay, /*wait_callback=*/false);
|
||||
const TransactionError err = BroadcastTransaction(m_node, tx, err_string, max_tx_fee, max_tx_fee_rate, relay, /*wait_callback=*/false);
|
||||
// Chain clients only care about failures to accept the tx to the mempool. Disregard non-mempool related failures.
|
||||
// Note: this will need to be updated if BroadcastTransactions() is updated to return other non-mempool failures
|
||||
// that Chain clients do not need to know about.
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <node/blockstorage.h>
|
||||
#include <node/context.h>
|
||||
#include <node/types.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <txmempool.h>
|
||||
#include <validation.h>
|
||||
#include <validationinterface.h>
|
||||
|
@ -31,7 +32,7 @@ static TransactionError HandleATMPError(const TxValidationState& state, std::str
|
|||
}
|
||||
}
|
||||
|
||||
TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, bool relay, bool wait_callback)
|
||||
TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, const CFeeRate& max_tx_fee_rate, bool relay, bool wait_callback)
|
||||
{
|
||||
// BroadcastTransaction can be called by RPC or by the wallet.
|
||||
// chainman, mempool and peerman are initialized before the RPC server and wallet are started
|
||||
|
@ -69,14 +70,16 @@ TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef t
|
|||
wtxid = mempool_tx->GetWitnessHash();
|
||||
} else {
|
||||
// Transaction is not already in the mempool.
|
||||
if (max_tx_fee > 0) {
|
||||
if (max_tx_fee > 0 || max_tx_fee_rate > CFeeRate(0)) {
|
||||
// First, call ATMP with test_accept and check the fee. If ATMP
|
||||
// fails here, return error immediately.
|
||||
const MempoolAcceptResult result = node.chainman->ProcessTransaction(tx, /*test_accept=*/ true);
|
||||
if (result.m_result_type != MempoolAcceptResult::ResultType::VALID) {
|
||||
return HandleATMPError(result.m_state, err_string);
|
||||
} else if (result.m_base_fees.value() > max_tx_fee) {
|
||||
} else if (max_tx_fee > 0 && result.m_base_fees.value() > max_tx_fee) {
|
||||
return TransactionError::MAX_FEE_EXCEEDED;
|
||||
} else if (max_tx_fee_rate > CFeeRate(0) && CFeeRate(result.m_base_fees.value(), result.m_vsize.value()) > max_tx_fee_rate) {
|
||||
return TransactionError::MAX_FEE_RATE_EXCEEDED;
|
||||
}
|
||||
}
|
||||
// Try to submit the transaction to the mempool.
|
||||
|
|
|
@ -44,12 +44,13 @@ static const CAmount DEFAULT_MAX_BURN_AMOUNT{0};
|
|||
* @param[in] node reference to node context
|
||||
* @param[in] tx the transaction to broadcast
|
||||
* @param[out] err_string reference to std::string to fill with error string if available
|
||||
* @param[in] max_tx_fee reject txs with fees higher than this (if 0, accept any fee)
|
||||
* @param[in] max_tx_fee reject txs with fees higher than this (if 0, the fee is not checked)
|
||||
* @param[in] max_tx_fee_rate reject txs with fee rate higher than this (if CFeeRate(0), the fee rate is not checked)
|
||||
* @param[in] relay flag if both mempool insertion and p2p relay are requested
|
||||
* @param[in] wait_callback wait until callbacks have been processed to avoid stale result due to a sequentially RPC.
|
||||
* return error
|
||||
*/
|
||||
[[nodiscard]] TransactionError BroadcastTransaction(NodeContext& node, CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, bool relay, bool wait_callback);
|
||||
[[nodiscard]] TransactionError BroadcastTransaction(NodeContext& node, CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, const CFeeRate& max_tx_fee_rate, bool relay, bool wait_callback);
|
||||
|
||||
/**
|
||||
* Return transaction with a given hash.
|
||||
|
|
|
@ -24,6 +24,7 @@ enum class TransactionError {
|
|||
MEMPOOL_REJECTED,
|
||||
MEMPOOL_ERROR,
|
||||
MAX_FEE_EXCEEDED,
|
||||
MAX_FEE_RATE_EXCEEDED,
|
||||
MAX_BURN_EXCEEDED,
|
||||
INVALID_PACKAGE,
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <key_io.h>
|
||||
#include <node/psbt.h>
|
||||
#include <node/types.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <policy/policy.h>
|
||||
#include <qt/bitcoinunits.h>
|
||||
#include <qt/forms/ui_psbtoperationsdialog.h>
|
||||
|
@ -120,7 +121,7 @@ void PSBTOperationsDialog::broadcastTransaction()
|
|||
CTransactionRef tx = MakeTransactionRef(mtx);
|
||||
std::string err_string;
|
||||
TransactionError error =
|
||||
m_client_model->node().broadcastTransaction(tx, DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK(), err_string);
|
||||
m_client_model->node().broadcastTransaction(tx, DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK(), CFeeRate(DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK()), err_string);
|
||||
|
||||
if (error == TransactionError::OK) {
|
||||
showStatus(tr("Transaction broadcast successfully! Transaction ID: %1")
|
||||
|
|
|
@ -228,7 +228,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
|
|||
|
||||
// Reject absurdly high fee. (This can never happen because the
|
||||
// wallet never creates transactions with fee greater than
|
||||
// m_default_max_tx_fee. This merely a belt-and-suspenders check).
|
||||
// m_max_tx_fee. This merely a belt-and-suspenders check).
|
||||
if (nFeeRequired > m_wallet->getDefaultMaxTxFee()) {
|
||||
return AbsurdFee;
|
||||
}
|
||||
|
|
|
@ -90,13 +90,11 @@ static RPCHelpMan sendrawtransaction()
|
|||
|
||||
const CFeeRate max_raw_tx_fee_rate{ParseFeeRate(self.Arg<UniValue>("maxfeerate"))};
|
||||
|
||||
int64_t virtual_size = GetVirtualTransactionSize(*tx);
|
||||
CAmount max_raw_tx_fee = max_raw_tx_fee_rate.GetFee(virtual_size);
|
||||
|
||||
std::string err_string;
|
||||
AssertLockNotHeld(cs_main);
|
||||
NodeContext& node = EnsureAnyNodeContext(request.context);
|
||||
const TransactionError err = BroadcastTransaction(node, tx, err_string, max_raw_tx_fee, /*relay=*/true, /*wait_callback=*/true);
|
||||
const TransactionError err = BroadcastTransaction(node, tx, err_string, /*max_tx_fee=*/0, max_raw_tx_fee_rate, /*relay=*/true, /*wait_callback=*/true);
|
||||
if (TransactionError::OK != err) {
|
||||
throw JSONRPCTransactionError(err, err_string);
|
||||
}
|
||||
|
@ -1060,7 +1058,7 @@ static RPCHelpMan submitpackage()
|
|||
|
||||
// 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);
|
||||
const auto err = BroadcastTransaction(node, tx, err_string, /*max_tx_fee=*/0, /*max_tx_fee_rate=*/CFeeRate(0), /*relay=*/true, /*wait_callback=*/true);
|
||||
if (err != TransactionError::OK) {
|
||||
throw JSONRPCTransactionError(err,
|
||||
strprintf("transaction broadcast failed: %s (%d transactions were broadcast successfully)",
|
||||
|
|
|
@ -27,6 +27,7 @@ constexpr TransactionError ALL_TRANSACTION_ERROR[] = {
|
|||
TransactionError::MEMPOOL_REJECTED,
|
||||
TransactionError::MEMPOOL_ERROR,
|
||||
TransactionError::MAX_FEE_EXCEEDED,
|
||||
TransactionError::MAX_FEE_RATE_EXCEEDED,
|
||||
};
|
||||
}; // namespace
|
||||
|
||||
|
|
|
@ -81,6 +81,13 @@ static feebumper::Result CheckFeeRate(const CWallet& wallet, const CMutableTrans
|
|||
return feebumper::Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
// check that new fee rate does not exceed maxfeerate
|
||||
if (newFeerate > wallet.m_max_tx_fee_rate) {
|
||||
errors.push_back(Untranslated(strprintf("New fee rate %s %s/kvB is too high (cannot be higher than -maxfeerate %s %s/kvB)",
|
||||
FormatMoney(newFeerate.GetFeePerK()), CURRENCY_UNIT, FormatMoney(wallet.m_max_tx_fee_rate.GetFeePerK()), CURRENCY_UNIT)));
|
||||
return feebumper::Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
std::vector<COutPoint> reused_inputs;
|
||||
reused_inputs.reserve(mtx.vin.size());
|
||||
for (const CTxIn& txin : mtx.vin) {
|
||||
|
@ -112,7 +119,7 @@ static feebumper::Result CheckFeeRate(const CWallet& wallet, const CMutableTrans
|
|||
}
|
||||
|
||||
// Check that in all cases the new fee doesn't violate maxTxFee
|
||||
const CAmount max_tx_fee = wallet.m_default_max_tx_fee;
|
||||
const CAmount max_tx_fee = wallet.m_max_tx_fee;
|
||||
if (new_total_fee > max_tx_fee) {
|
||||
errors.push_back(Untranslated(strprintf("Specified or calculated fee %s is too high (cannot be higher than -maxtxfee %s)",
|
||||
FormatMoney(new_total_fee), FormatMoney(max_tx_fee))));
|
||||
|
|
|
@ -64,6 +64,7 @@ void WalletInit::AddWalletOptions(ArgsManager& argsman) const
|
|||
argsman.AddArg("-maxapsfee=<n>", strprintf("Spend up to this amount in additional (absolute) fees (in %s) if it allows the use of partial spend avoidance (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_MAX_AVOIDPARTIALSPEND_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-maxtxfee=<amt>", strprintf("Maximum total fees (in %s) to use in a single wallet transaction; setting this too low may abort large transactions (default: %s)",
|
||||
CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MAXFEE)), ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
|
||||
argsman.AddArg("-maxfeerate=<amt>", strprintf("Maximum fee rate (in %s/kvB) for a wallet transactions (default: %s %s/kvB)", CURRENCY_UNIT, FormatMoney(DEFAULT_MAX_TRANSACTION_FEERATE.GetFeePerK()), CURRENCY_UNIT), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-mintxfee=<amt>", strprintf("Fee rates (in %s/kvB) smaller than this are considered zero fee for transaction creation (default: %s)",
|
||||
CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MINFEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-paytxfee=<amt>", strprintf("Fee rate (in %s/kvB) to add to transactions you send (default: %s)",
|
||||
|
|
|
@ -521,7 +521,7 @@ public:
|
|||
return spk_man != nullptr;
|
||||
}
|
||||
OutputType getDefaultAddressType() override { return m_wallet->m_default_address_type; }
|
||||
CAmount getDefaultMaxTxFee() override { return m_wallet->m_default_max_tx_fee; }
|
||||
CAmount getDefaultMaxTxFee() override { return m_wallet->m_max_tx_fee; }
|
||||
void remove() override
|
||||
{
|
||||
RemoveWallet(m_context, m_wallet, /*load_on_start=*/false);
|
||||
|
|
|
@ -439,15 +439,14 @@ RPCHelpMan settxfee()
|
|||
|
||||
CAmount nAmount = AmountFromValue(request.params[0]);
|
||||
CFeeRate tx_fee_rate(nAmount, 1000);
|
||||
CFeeRate max_tx_fee_rate(pwallet->m_default_max_tx_fee, 1000);
|
||||
if (tx_fee_rate == CFeeRate(0)) {
|
||||
// automatic selection
|
||||
} else if (tx_fee_rate < pwallet->chain().relayMinFee()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("txfee cannot be less than min relay tx fee (%s)", pwallet->chain().relayMinFee().ToString()));
|
||||
} else if (tx_fee_rate < pwallet->m_min_fee) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("txfee cannot be less than wallet min fee (%s)", pwallet->m_min_fee.ToString()));
|
||||
} else if (tx_fee_rate > max_tx_fee_rate) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("txfee cannot be more than wallet max tx fee (%s)", max_tx_fee_rate.ToString()));
|
||||
} else if (tx_fee_rate > pwallet->m_max_tx_fee_rate) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("fee rate cannot be more than wallet max tx fee rate (%s)", pwallet->m_max_tx_fee_rate.ToString()));
|
||||
}
|
||||
|
||||
pwallet->m_pay_tx_fee = tx_fee_rate;
|
||||
|
@ -1506,9 +1505,12 @@ RPCHelpMan sendall()
|
|||
const std::optional<CAmount> total_bump_fees{pwallet->chain().calculateCombinedBumpFee(outpoints_spent, fee_rate)};
|
||||
CAmount effective_value = total_input_value - fee_from_size - total_bump_fees.value_or(0);
|
||||
|
||||
if (fee_from_size > pwallet->m_default_max_tx_fee) {
|
||||
if (fee_from_size > pwallet->m_max_tx_fee) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED).original);
|
||||
}
|
||||
if (CFeeRate(fee_from_size, tx_size.vsize) > pwallet->m_max_tx_fee_rate) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, TransactionErrorString(TransactionError::MAX_FEE_RATE_EXCEEDED).original);
|
||||
}
|
||||
|
||||
if (effective_value <= 0) {
|
||||
if (send_max) {
|
||||
|
|
|
@ -1339,10 +1339,15 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
|
|||
return util::Error{_("Transaction too large")};
|
||||
}
|
||||
|
||||
if (current_fee > wallet.m_default_max_tx_fee) {
|
||||
if (current_fee > wallet.m_max_tx_fee) {
|
||||
return util::Error{TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED)};
|
||||
}
|
||||
|
||||
CFeeRate tx_fee_rate = CFeeRate(current_fee, nBytes);
|
||||
if (tx_fee_rate > wallet.m_max_tx_fee_rate) {
|
||||
return util::Error{TransactionErrorString(TransactionError::MAX_FEE_RATE_EXCEEDED)};
|
||||
}
|
||||
|
||||
if (gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS)) {
|
||||
// Lastly, ensure this tx will pass the mempool's chain limits
|
||||
auto result = wallet.chain().checkChainLimits(tx);
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include <interfaces/chain.h>
|
||||
#include <key_io.h>
|
||||
#include <node/blockstorage.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <policy/policy.h>
|
||||
#include <rpc/server.h>
|
||||
#include <script/solver.h>
|
||||
|
@ -858,7 +859,8 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
|
|||
auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
|
||||
auto tx_vsize = GetVirtualTransactionSize(CTransaction{mempool_tx});
|
||||
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, CFeeRate(tx_vsize, DEFAULT_TRANSACTION_MAXFEE), false, error));
|
||||
|
||||
|
||||
// Reload wallet and make sure new transactions are detected despite events
|
||||
|
@ -900,7 +902,8 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
|
|||
block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
|
||||
auto tx_vsize = GetVirtualTransactionSize(CTransaction{mempool_tx});
|
||||
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, CFeeRate(tx_vsize, DEFAULT_TRANSACTION_MAXFEE), false, error));
|
||||
m_node.validation_signals->SyncWithValidationInterfaceQueue();
|
||||
});
|
||||
wallet = TestLoadWallet(context);
|
||||
|
|
|
@ -2053,7 +2053,7 @@ bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string
|
|||
// If broadcast fails for any reason, trying to set wtx.m_state here would be incorrect.
|
||||
// If transaction was previously in the mempool, it should be updated when
|
||||
// TransactionRemovedFromMempool fires.
|
||||
bool ret = chain().broadcastTransaction(wtx.tx, m_default_max_tx_fee, relay, err_string);
|
||||
bool ret = chain().broadcastTransaction(wtx.tx, m_max_tx_fee, m_max_tx_fee_rate, relay, err_string);
|
||||
if (ret) wtx.m_state = TxStateInMempool{};
|
||||
return ret;
|
||||
}
|
||||
|
@ -3124,17 +3124,17 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
|
|||
}
|
||||
|
||||
if (args.IsArgSet("-maxapsfee")) {
|
||||
const std::string max_aps_fee{args.GetArg("-maxapsfee", "")};
|
||||
if (max_aps_fee == "-1") {
|
||||
const std::string max_aps_fee_str{args.GetArg("-maxapsfee", "")};
|
||||
if (max_aps_fee_str == "-1") {
|
||||
walletInstance->m_max_aps_fee = -1;
|
||||
} else if (std::optional<CAmount> max_fee = ParseMoney(max_aps_fee)) {
|
||||
if (max_fee.value() > HIGH_APS_FEE) {
|
||||
} else if (std::optional<CAmount> max_aps_fee = ParseMoney(max_aps_fee_str)) {
|
||||
if (max_aps_fee.value() > HIGH_APS_FEE) {
|
||||
warnings.push_back(AmountHighWarn("-maxapsfee") + Untranslated(" ") +
|
||||
_("This is the maximum transaction fee you pay (in addition to the normal fee) to prioritize partial spend avoidance over regular coin selection."));
|
||||
}
|
||||
walletInstance->m_max_aps_fee = max_fee.value();
|
||||
walletInstance->m_max_aps_fee = max_aps_fee.value();
|
||||
} else {
|
||||
error = AmountErrMsg("maxapsfee", max_aps_fee);
|
||||
error = AmountErrMsg("maxapsfee", max_aps_fee_str);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
@ -3186,21 +3186,39 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
|
|||
}
|
||||
|
||||
if (args.IsArgSet("-maxtxfee")) {
|
||||
std::optional<CAmount> max_fee = ParseMoney(args.GetArg("-maxtxfee", ""));
|
||||
if (!max_fee) {
|
||||
std::optional<CAmount> max_tx_fee = ParseMoney(args.GetArg("-maxtxfee", ""));
|
||||
if (!max_tx_fee) {
|
||||
error = AmountErrMsg("maxtxfee", args.GetArg("-maxtxfee", ""));
|
||||
return nullptr;
|
||||
} else if (max_fee.value() > HIGH_MAX_TX_FEE) {
|
||||
} else if (max_tx_fee.value() > HIGH_MAX_TX_FEE) {
|
||||
warnings.push_back(strprintf(_("%s is set very high! Fees this large could be paid on a single transaction."), "-maxtxfee"));
|
||||
|
||||
// Wallet prevents creating transactions with fee rates lower than minrelaytxfee.
|
||||
// Also the wallet prevents creating transaction with base fee above maxtxfee.
|
||||
// Warn when a 1kvb transaction, with a base fee set to maxtxfee, has a fee rate less than minrelaytxfee.
|
||||
// It is likely that some transactions with fee rates greater than or equal to the minrelaytxfee will exceed maxtxfee.
|
||||
// In such cases, the wallet won't be able to create transactions. Therefore, warn the user.
|
||||
} else if (chain && CFeeRate(max_tx_fee.value(), 1000) < chain->relayMinFee()) {
|
||||
warnings.push_back(strprintf(_("Invalid amount for %s=<amount>: '%s' conflicts with the minimum relay transaction feerate %s. Please set a higher %s or lower %s"),
|
||||
"-maxtxfee", args.GetArg("-maxtxfee", ""), chain->relayMinFee().ToString(), "-maxtxfee", "-minrelaytxfee"));
|
||||
}
|
||||
|
||||
if (chain && CFeeRate{max_fee.value(), 1000} < chain->relayMinFee()) {
|
||||
walletInstance->m_max_tx_fee = max_tx_fee.value();
|
||||
}
|
||||
|
||||
if (args.IsArgSet("-maxfeerate")) {
|
||||
std::optional<CAmount> max_tx_fee_rate = ParseMoney(args.GetArg("-maxfeerate", ""));
|
||||
if (!max_tx_fee_rate) {
|
||||
error = AmountErrMsg("maxfeerate", args.GetArg("-maxfeerate", ""));
|
||||
return nullptr;
|
||||
}
|
||||
if (chain && CFeeRate(max_tx_fee_rate.value()) < chain->relayMinFee()) {
|
||||
error = strprintf(_("Invalid amount for %s=<amount>: '%s' (must be at least the minrelay fee of %s to prevent stuck transactions)"),
|
||||
"-maxtxfee", args.GetArg("-maxtxfee", ""), chain->relayMinFee().ToString());
|
||||
"-maxfeerate", args.GetArg("-maxfeerate", ""), chain->relayMinFee().ToString());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
walletInstance->m_default_max_tx_fee = max_fee.value();
|
||||
walletInstance->m_max_tx_fee_rate = CFeeRate(max_tx_fee_rate.value());
|
||||
}
|
||||
|
||||
if (args.IsArgSet("-consolidatefeerate")) {
|
||||
|
|
|
@ -135,6 +135,8 @@ static const bool DEFAULT_DISABLE_WALLET = false;
|
|||
static const bool DEFAULT_WALLETCROSSCHAIN = false;
|
||||
//! -maxtxfee default
|
||||
constexpr CAmount DEFAULT_TRANSACTION_MAXFEE{COIN / 10};
|
||||
//! maxfeerate default
|
||||
const CFeeRate DEFAULT_MAX_TRANSACTION_FEERATE(CFeeRate(COIN / 10));
|
||||
//! Discourage users to set fees higher than this amount (in satoshis) per kB
|
||||
constexpr CAmount HIGH_TX_FEE_PER_KB{COIN / 100};
|
||||
//! -maxtxfee will warn if called with a higher fee than this amount (in satoshis)
|
||||
|
@ -728,8 +730,10 @@ public:
|
|||
*/
|
||||
std::optional<OutputType> m_default_change_type{};
|
||||
/** Absolute maximum transaction fee (in satoshis) used by default for the wallet */
|
||||
CAmount m_default_max_tx_fee{DEFAULT_TRANSACTION_MAXFEE};
|
||||
CAmount m_max_tx_fee{DEFAULT_TRANSACTION_MAXFEE};
|
||||
|
||||
/** Maximum transaction fee rate used for the wallet */
|
||||
CFeeRate m_max_tx_fee_rate{DEFAULT_MAX_TRANSACTION_FEERATE};
|
||||
/** Number of pre-generated keys/scripts by each spkm (part of the look-ahead process, used to detect payments) */
|
||||
int64_t m_keypool_size{DEFAULT_KEYPOOL_SIZE};
|
||||
|
||||
|
|
|
@ -387,7 +387,7 @@ class PSBTTest(BitcoinTestFramework):
|
|||
|
||||
self.log.info("Test invalid fee rate settings")
|
||||
for param, value in {("fee_rate", 100000), ("feeRate", 1)}:
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (maxtxfee)",
|
||||
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: value, "add_inputs": True})
|
||||
assert_raises_rpc_error(-3, "Amount out of range",
|
||||
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: -1, "add_inputs": True})
|
||||
|
@ -441,7 +441,7 @@ class PSBTTest(BitcoinTestFramework):
|
|||
self.log.info("Test walletcreatefundedpsbt with too-high fee rate produces total fee well above -maxtxfee and raises RPC error")
|
||||
# previously this was silently capped at -maxtxfee
|
||||
for bool_add, outputs_array in {True: outputs, False: [{self.nodes[1].getnewaddress(): 1}]}.items():
|
||||
msg = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"
|
||||
msg = "Fee exceeds maximum configured by user (maxtxfee)"
|
||||
assert_raises_rpc_error(-4, msg, self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"fee_rate": 1000000, "add_inputs": bool_add})
|
||||
assert_raises_rpc_error(-4, msg, self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"feeRate": 1, "add_inputs": bool_add})
|
||||
|
||||
|
|
|
@ -400,7 +400,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
|
||||
def sendrawtransaction_testmempoolaccept_tests(self):
|
||||
self.log.info("Test sendrawtransaction/testmempoolaccept with maxfeerate")
|
||||
fee_exceeds_max = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"
|
||||
fee_rate_exceeds_max = "Fee rate exceeds maximum configured by user (maxfeerate)"
|
||||
|
||||
# Test a transaction with a small fee.
|
||||
# Fee rate is 0.00100000 BTC/kvB
|
||||
|
@ -410,7 +410,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
assert_equal(testres['allowed'], False)
|
||||
assert_equal(testres['reject-reason'], 'max-fee-exceeded')
|
||||
# and sendrawtransaction should throw
|
||||
assert_raises_rpc_error(-25, fee_exceeds_max, self.nodes[2].sendrawtransaction, tx['hex'], 0.00001000)
|
||||
assert_raises_rpc_error(-25, fee_rate_exceeds_max, self.nodes[2].sendrawtransaction, tx['hex'], 0.00001000)
|
||||
# and the following calls should both succeed
|
||||
testres = self.nodes[2].testmempoolaccept(rawtxs=[tx['hex']])[0]
|
||||
assert_equal(testres['allowed'], True)
|
||||
|
@ -424,7 +424,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
assert_equal(testres['allowed'], False)
|
||||
assert_equal(testres['reject-reason'], 'max-fee-exceeded')
|
||||
# and sendrawtransaction should throw
|
||||
assert_raises_rpc_error(-25, fee_exceeds_max, self.nodes[2].sendrawtransaction, tx['hex'])
|
||||
assert_raises_rpc_error(-25, fee_rate_exceeds_max, self.nodes[2].sendrawtransaction, tx['hex'])
|
||||
# and the following calls should both succeed
|
||||
testres = self.nodes[2].testmempoolaccept(rawtxs=[tx['hex']], maxfeerate='0.20000000')[0]
|
||||
assert_equal(testres['allowed'], True)
|
||||
|
|
|
@ -132,8 +132,11 @@ class BumpFeeTest(BitcoinTestFramework):
|
|||
assert_raises_rpc_error(-8, "Insufficient total fee 0.00000141", rbf_node.bumpfee, rbfid, fee_rate=INSUFFICIENT)
|
||||
|
||||
self.log.info("Test invalid fee rate settings")
|
||||
assert_raises_rpc_error(-4, "Specified or calculated fee 0.141 is too high (cannot be higher than -maxtxfee 0.10",
|
||||
|
||||
# Bumping to a very high fee rate above the default -maxfeerate should fail
|
||||
assert_raises_rpc_error(-4, "New fee rate 1.00 BTC/kvB is too high (cannot be higher than -maxfeerate 0.10 BTC/kvB)",
|
||||
rbf_node.bumpfee, rbfid, fee_rate=TOO_HIGH)
|
||||
|
||||
# Test fee_rate with zero values.
|
||||
msg = "Insufficient total fee 0.00"
|
||||
for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]:
|
||||
|
@ -547,9 +550,9 @@ def test_settxfee(self, rbf_node, dest_address):
|
|||
assert_greater_than(Decimal("0.00001000"), abs(requested_feerate - actual_feerate))
|
||||
rbf_node.settxfee(Decimal("0.00000000")) # unset paytxfee
|
||||
|
||||
# check that settxfee respects -maxtxfee
|
||||
self.restart_node(1, ['-maxtxfee=0.000025'] + self.extra_args[1])
|
||||
assert_raises_rpc_error(-8, "txfee cannot be more than wallet max tx fee", rbf_node.settxfee, Decimal('0.00003'))
|
||||
# check that settxfee respects -maxfeerate
|
||||
self.restart_node(1, ['-maxfeerate=0.000025'] + self.extra_args[1])
|
||||
assert_raises_rpc_error(-8, "fee rate cannot be more than wallet max tx fee rate", rbf_node.settxfee, Decimal('0.00003'))
|
||||
self.restart_node(1, self.extra_args[1])
|
||||
rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
|
||||
self.connect_nodes(1, 0)
|
||||
|
@ -564,7 +567,24 @@ def test_maxtxfee_fails(self, rbf_node, dest_address):
|
|||
self.restart_node(1, ['-maxtxfee=0.000025'] + self.extra_args[1])
|
||||
rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
|
||||
rbfid = spend_one_input(rbf_node, dest_address)
|
||||
assert_raises_rpc_error(-4, "Unable to create transaction. Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", rbf_node.bumpfee, rbfid)
|
||||
assert_raises_rpc_error(-4, "Unable to create transaction. Fee exceeds maximum configured by user (maxtxfee)", rbf_node.bumpfee, rbfid)
|
||||
|
||||
# When user passed fee rate causes base fee to be above maxtxfee we fail early
|
||||
assert_raises_rpc_error(-4, "Specified or calculated fee 0.0000282 is too high (cannot be higher than -maxtxfee 0.000025)", rbf_node.bumpfee, rbfid, fee_rate=20)
|
||||
|
||||
self.log.info("Test that a low -maxtxfee, which may prevent tx fee rate from reaching -minrelaytxfee triggers a warning.")
|
||||
low_max_tx_fee = '0.000001'
|
||||
high_max_tx_fee = '0.001'
|
||||
high_min_relay_fee = '0.0002'
|
||||
msg = f"Invalid amount for -maxtxfee=<amount>: '{low_max_tx_fee}' conflicts with the minimum relay transaction feerate {format(float(high_min_relay_fee), '.8f')} BTC/kvB. Please set a higher -maxtxfee or lower -minrelaytxfee"
|
||||
self.restart_node(1, extra_args=[f'-minrelaytxfee={high_min_relay_fee}', f'-maxtxfee={low_max_tx_fee}'])
|
||||
warnings = self.nodes[1].createwallet("test-wallet")["warnings"]
|
||||
assert msg in warnings
|
||||
|
||||
self.log.info("Test that a -maxtxfee high enough to allow tx fee rate to meet or exceed -minrelaytxfee should start normally.")
|
||||
msg = "Warning: " + msg
|
||||
self.stop_node(1, expected_stderr=msg)
|
||||
self.start_node(1, extra_args=[f'-minrelaytxfee={high_min_relay_fee}', f'-maxtxfee={high_max_tx_fee}'])
|
||||
self.restart_node(1, self.extra_args[1])
|
||||
rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
|
||||
self.connect_nodes(1, 0)
|
||||
|
|
|
@ -61,12 +61,12 @@ class CreateTxWalletTest(BitcoinTestFramework):
|
|||
self.restart_node(0, extra_args=[fee_setting])
|
||||
assert_raises_rpc_error(
|
||||
-6,
|
||||
"Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
"Fee exceeds maximum configured by user (maxtxfee)",
|
||||
lambda: self.nodes[0].sendmany(dummy="", amounts=outputs),
|
||||
)
|
||||
assert_raises_rpc_error(
|
||||
-4,
|
||||
"Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
"Fee exceeds maximum configured by user (maxtxfee)",
|
||||
lambda: self.nodes[0].fundrawtransaction(hexstring=raw_tx),
|
||||
)
|
||||
|
||||
|
@ -75,12 +75,12 @@ class CreateTxWalletTest(BitcoinTestFramework):
|
|||
self.nodes[0].settxfee(0.01)
|
||||
assert_raises_rpc_error(
|
||||
-6,
|
||||
"Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
"Fee exceeds maximum configured by user (maxtxfee)",
|
||||
lambda: self.nodes[0].sendmany(dummy="", amounts=outputs),
|
||||
)
|
||||
assert_raises_rpc_error(
|
||||
-4,
|
||||
"Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
"Fee exceeds maximum configured by user (maxtxfee)",
|
||||
lambda: self.nodes[0].fundrawtransaction(hexstring=raw_tx),
|
||||
)
|
||||
self.nodes[0].settxfee(0)
|
||||
|
|
|
@ -853,7 +853,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
|
||||
self.log.info("Test invalid fee rate settings")
|
||||
for param, value in {("fee_rate", 100000), ("feeRate", 1.000)}:
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (maxtxfee)",
|
||||
node.fundrawtransaction, rawtx, add_inputs=True, **{param: value})
|
||||
assert_raises_rpc_error(-3, "Amount out of range",
|
||||
node.fundrawtransaction, rawtx, add_inputs=True, **{param: -1})
|
||||
|
|
|
@ -184,6 +184,30 @@ class WalletSendTest(BitcoinTestFramework):
|
|||
|
||||
return res
|
||||
|
||||
def test_maxfeerate(self):
|
||||
self.log.info("test -maxfeerate enforcement on wallet transactions.")
|
||||
# Default maxfeerate is 10,000 sat/vB
|
||||
# Wallet will reject all transactions with fee rate above 10,000 sat/vB.
|
||||
assert_raises_rpc_error(-6, "Fee rate exceeds maximum configured by user (maxfeerate)",
|
||||
self.nodes[0].sendtoaddress, address=self.nodes[0].getnewaddress(), amount=1, fee_rate=10001)
|
||||
|
||||
# All transaction with fee rate <= 10,000 sat/vB can be created.
|
||||
self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), amount=1, fee_rate=9900)
|
||||
|
||||
# Configure a lower -maxfeerate of 10 sat/vB.
|
||||
self.restart_node(0, extra_args=['-maxfeerate=0.00010'])
|
||||
|
||||
# Wallet will reject all transactions with fee rate above 10 sat/vB.
|
||||
assert_raises_rpc_error(-6, "Fee rate exceeds maximum configured by user (maxfeerate)",
|
||||
self.nodes[0].sendtoaddress, address=self.nodes[0].getnewaddress(), amount=1, fee_rate=11)
|
||||
|
||||
# Fee rates <= 10 sat/vB will be accepted by the wallet.
|
||||
self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), amount=1, fee_rate=9)
|
||||
|
||||
# Restart the node with the default -maxfeerate option
|
||||
self.restart_node(0)
|
||||
|
||||
|
||||
def run_test(self):
|
||||
self.log.info("Setup wallets...")
|
||||
# w0 is a wallet with coinbase rewards
|
||||
|
@ -580,6 +604,9 @@ class WalletSendTest(BitcoinTestFramework):
|
|||
# Check tx creation size limits
|
||||
self.test_weight_limits()
|
||||
|
||||
self.test_maxfeerate()
|
||||
|
||||
|
||||
def test_weight_limits(self):
|
||||
self.log.info("Test weight limits")
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue