diff --git a/src/wallet/feebumper.cpp b/src/wallet/feebumper.cpp index f3acb836f17223..f37199db7efc99 100644 --- a/src/wallet/feebumper.cpp +++ b/src/wallet/feebumper.cpp @@ -80,6 +80,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(strprintf(Untranslated("New fee rate %s is too high (cannot be higher than -maxfeerate %s)"), + FormatMoney(newFeerate.GetFeePerK()), FormatMoney(wallet.m_max_tx_fee_rate.GetFeePerK()))); + return feebumper::Result::WALLET_ERROR; + } + std::vector reused_inputs; reused_inputs.reserve(mtx.vin.size()); for (const CTxIn& txin : mtx.vin) { diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index a00240ed0f2bd4..6817ef1156d599 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -1486,6 +1486,9 @@ RPCHelpMan sendall() 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) { diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 2867a4a66c3761..49e054747cbfd7 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -1299,6 +1299,11 @@ static util::Result CreateTransactionInternal( 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); diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index 3292f2711b2804..277483eee3dabe 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -130,8 +130,11 @@ def test_invalid_parameters(self, rbf_node, peer_node, dest_address): 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 is too high (cannot be higher than -maxfeerate 0.10)", 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"]: @@ -563,6 +566,9 @@ def test_maxtxfee_fails(self, rbf_node, dest_address): 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 (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.restart_node(1, self.extra_args[1]) rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT) self.connect_nodes(1, 0) diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index 6ce2a56bfcbbc1..e0dd3c21ac6042 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -184,6 +184,30 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None, return res + def test_maxfeerate(self): + self.log.info("test -maxfeerate enforcement on wallet transactions.") + # Default maxfeerate is 10,000 sats/vb + # Wallet will reject all transactions with feerate above 10,000 sats/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 feerate <= 10,000 sats/vb can be created. + self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), amount=1, fee_rate=9900) + + # Configure a lower -maxfeerate of 10 sats/vb. + self.restart_node(0, extra_args=['-maxfeerate=0.00010']) + + # Wallet will reject all transactions with fee rate above 10 sats/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 sats/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 @@ -575,6 +599,7 @@ def run_test(self): testres = self.nodes[0].testmempoolaccept([signed["hex"]])[0] assert_equal(testres["allowed"], True) assert_fee_amount(testres["fees"]["base"], testres["vsize"], Decimal(0.0001)) + self.test_maxfeerate() if __name__ == '__main__': WalletSendTest().main()