Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: interfaces, make 'createTransaction' less error-prone #807

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum class TransactionError;
struct PartiallySignedTransaction;
struct bilingual_str;
namespace wallet {
struct CreatedTransactionResult;
class CCoinControl;
class CWallet;
enum class AddressPurpose;
Expand Down Expand Up @@ -142,11 +143,10 @@ class Wallet
virtual void listLockedCoins(std::vector<COutPoint>& outputs) = 0;

//! Create transaction.
virtual util::Result<CTransactionRef> createTransaction(const std::vector<wallet::CRecipient>& recipients,
virtual util::Result<wallet::CreatedTransactionResult> createTransaction(const std::vector<wallet::CRecipient>& recipients,
const wallet::CCoinControl& coin_control,
bool sign,
int& change_pos,
CAmount& fee) = 0;
std::optional<unsigned int> change_pos) = 0;

//! Commit transaction.
virtual void commitTransaction(CTransactionRef tx,
Expand Down
29 changes: 13 additions & 16 deletions src/qt/walletmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <psbt.h>
#include <util/translation.h>
#include <wallet/coincontrol.h>
#include <wallet/types.h>
#include <wallet/wallet.h> // for CRecipient

#include <stdint.h>
Expand Down Expand Up @@ -153,6 +154,8 @@ bool WalletModel::validateAddress(const QString& address) const

WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction &transaction, const CCoinControl& coinControl)
{
transaction.getWtx() = nullptr; // reset tx output

CAmount total = 0;
bool fSubtractFeeFromAmount = false;
QList<SendCoinsRecipient> recipients = transaction.getRecipients();
Expand Down Expand Up @@ -204,27 +207,21 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
}

try {
CAmount nFeeRequired = 0;
int nChangePosRet = -1;

auto& newTx = transaction.getWtx();
const auto& res = m_wallet->createTransaction(vecSend, coinControl, /*sign=*/!wallet().privateKeysDisabled(), nChangePosRet, nFeeRequired);
newTx = res ? *res : nullptr;
transaction.setTransactionFee(nFeeRequired);
if (fSubtractFeeFromAmount && newTx)
transaction.reassignAmounts(nChangePosRet);

if(!newTx)
{
if(!fSubtractFeeFromAmount && (total + nFeeRequired) > nBalance)
{
return SendCoinsReturn(AmountWithFeeExceedsBalance);
Copy link
Contributor

@ryanofsky ryanofsky May 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "gui: remove unreachable AmountWithFeeExceedsBalance error" (355199c)

This is a nice catch noticing that this code doesn't work. But I think the fact that it doesn't work is a pretty unfortunate problem, and the current PR will make fixing it harder.

It seems like bitcoin/bitcoin#20640 unintentionally introduced a bug where the more accurate error message "The total exceeds your balance when the %1 transaction fee is included" can never trigger, and instead only less informative "Insufficient funds" or "The amount exceeds your balance" errors will be shown.

It looks like there are few ways this could be fixed:

  • An easy way to fix it would be to restore the CAmount& fee output parameter removed by #20640 so it is always returned and this code can work the way it was originally intended.

  • A slightly more complicated but cleaner fix would be to use CreatedTransactionResult as a return type instead of util::Result<CreatedTransactionResult> and add a bilingual_str error field to CreatedTransactionResult so fee information and an error string can be returned together.

  • Another alternative fix could be to build on bitcoin/bitcoin#25665 and keep using util::Result, but return the fee in the new util::Result failure field.

  • And the best but probably most complicated fix would be to remove error message code from the GUI and instead generate detailed error messages in wallet code. This would require bigger changes in the GUI code and might require changing the interface to provide the wallet with more information about transactions.

Against that background, I think this PR (as of edc3f9a) makes some nice cleanups, but I don't like the current changes because I think they make all of the fixes above except maybe the last one harder to implement.

So I don't think I would ACK this PR currently, I'd happily ACK it if it either (1) fixed the problem, maybe using one of the easier fixes suggested above, or (2) did not fix the problem, but at least did not make it harder to fix in the future. This could be implemented by keeping most of the changes in the PR, but not deleting this GUI code in this commit and not deleting the CAmount& fee output parameter from the wallet interface after this commit.

}
const auto& res = m_wallet->createTransaction(vecSend, coinControl, /*sign=*/!wallet().privateKeysDisabled(), /*change_pos=*/std::nullopt);
if (!res) {
Q_EMIT message(tr("Send Coins"), QString::fromStdString(util::ErrorString(res).translated),
CClientUIInterface::MSG_ERROR);
CClientUIInterface::MSG_ERROR);
return TransactionCreationFailed;
}

newTx = res->tx;
CAmount nFeeRequired = res->fee;
transaction.setTransactionFee(nFeeRequired);
if (fSubtractFeeFromAmount && newTx) {
transaction.reassignAmounts(res->change_pos ? int(*res->change_pos) : -1);
}

// 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).
Expand Down
14 changes: 3 additions & 11 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -274,21 +274,13 @@ class WalletImpl : public Wallet
LOCK(m_wallet->cs_wallet);
return m_wallet->ListLockedCoins(outputs);
}
util::Result<CTransactionRef> createTransaction(const std::vector<CRecipient>& recipients,
util::Result<wallet::CreatedTransactionResult> createTransaction(const std::vector<CRecipient>& recipients,
const CCoinControl& coin_control,
bool sign,
int& change_pos,
CAmount& fee) override
std::optional<unsigned int> change_pos) override
{
LOCK(m_wallet->cs_wallet);
auto res = CreateTransaction(*m_wallet, recipients, change_pos == -1 ? std::nullopt : std::make_optional(change_pos),
coin_control, sign);
if (!res) return util::Error{util::ErrorString(res)};
const auto& txr = *res;
fee = txr.fee;
change_pos = txr.change_pos ? int(*txr.change_pos) : -1;

return txr.tx;
return CreateTransaction(*m_wallet, recipients, change_pos, coin_control, sign);
}
void commitTransaction(CTransactionRef tx,
WalletValueMap value_map,
Expand Down
14 changes: 13 additions & 1 deletion src/wallet/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1131,7 +1131,19 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
if (!select_coins_res) {
// 'SelectCoins' either returns a specific error message or, if empty, means a general "Insufficient funds".
const bilingual_str& err = util::ErrorString(select_coins_res);
return util::Error{err.empty() ?_("Insufficient funds") : err};
if (!err.empty()) return util::Error{err};

// Check if we have enough balance but cannot cover the fees
CAmount available_balance = preset_inputs.total_amount + available_coins.GetTotalAmount();
if (available_balance >= recipients_sum) {
CAmount available_effective_balance = preset_inputs.total_amount + available_coins.GetEffectiveTotalAmount().value_or(available_coins.GetTotalAmount());
if (available_effective_balance < selection_target) {
return util::Error{_("The total transaction amount exceeds your balance when fees are included")};
}
}

// General failure description
return util::Error{_("Insufficient funds")};
}
const SelectionResult& result = *select_coins_res;
TRACE5(coin_selection, selected_coins,
Expand Down
12 changes: 1 addition & 11 deletions src/wallet/spend.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <util/result.h>
#include <wallet/coinselection.h>
#include <wallet/transaction.h>
#include <wallet/types.h>
#include <wallet/wallet.h>

#include <optional>
Expand Down Expand Up @@ -202,17 +203,6 @@ util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& av
const CAmount& nTargetValue, const CCoinControl& coin_control,
const CoinSelectionParams& coin_selection_params) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);

struct CreatedTransactionResult
{
CTransactionRef tx;
CAmount fee;
FeeCalculation fee_calc;
std::optional<unsigned int> change_pos;

CreatedTransactionResult(CTransactionRef _tx, CAmount _fee, std::optional<unsigned int> _change_pos, const FeeCalculation& _fee_calc)
: tx(_tx), fee(_fee), fee_calc(_fee_calc), change_pos(_change_pos) {}
};

/**
* Create a new transaction paying the recipients with a set of coins
* selected by SelectCoins(); Also create the change output, when needed
Expand Down
14 changes: 14 additions & 0 deletions src/wallet/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#ifndef BITCOIN_WALLET_TYPES_H
#define BITCOIN_WALLET_TYPES_H

#include <policy/fees.h>

#include <type_traits>

namespace wallet {
Expand Down Expand Up @@ -62,6 +64,18 @@ enum class AddressPurpose {
SEND,
REFUND, //!< Never set in current code may be present in older wallet databases
};

struct CreatedTransactionResult
{
CTransactionRef tx;
CAmount fee;
FeeCalculation fee_calc;
std::optional<unsigned int> change_pos;

CreatedTransactionResult(CTransactionRef _tx, CAmount _fee, std::optional<unsigned int> _change_pos, const FeeCalculation& _fee_calc)
: tx(_tx), fee(_fee), fee_calc(_fee_calc), change_pos(_change_pos) {}
};

} // namespace wallet

#endif // BITCOIN_WALLET_TYPES_H
19 changes: 18 additions & 1 deletion test/functional/wallet_fundrawtransaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def run_test(self):
self.test_feerate_rounding()
self.test_input_confs_control()
self.test_duplicate_outputs()
self.test_cannot_cover_fees()

def test_duplicate_outputs(self):
self.log.info("Test deserializing and funding a transaction with duplicate outputs")
Expand Down Expand Up @@ -1426,7 +1427,8 @@ def test_feerate_rounding(self):
# To test this does not happen, we subtract 202 sats from the input value. If working correctly, this should
# fail with insufficient funds rather than bitcoind asserting.
rawtx = w.createrawtransaction(inputs=[], outputs=[{self.nodes[0].getnewaddress(address_type="bech32"): 1 - 0.00000202}])
assert_raises_rpc_error(-4, "Insufficient funds", w.fundrawtransaction, rawtx, fee_rate=1.85)
expected_err_msg = "The total transaction amount exceeds your balance when fees are included"
assert_raises_rpc_error(-4, expected_err_msg, w.fundrawtransaction, rawtx, fee_rate=1.85)

def test_input_confs_control(self):
self.nodes[0].createwallet("minconf")
Expand Down Expand Up @@ -1489,5 +1491,20 @@ def test_input_confs_control(self):

wallet.unloadwallet()

def test_cannot_cover_fees(self):
self.log.info("Test tx amount exceeds available balance when fees are included")

self.nodes[1].createwallet("cannot_cover_fees")
wallet = self.nodes[1].get_wallet_rpc("cannot_cover_fees")

self.nodes[0].sendtoaddress(wallet.getnewaddress(), 0.3)
self.generate(self.nodes[0], 1)

rawtx = wallet.createrawtransaction(inputs=[], outputs=[{self.nodes[0].getnewaddress(): 0.3}])
expected_err_msg = "The total transaction amount exceeds your balance when fees are included"
assert_raises_rpc_error(-4, expected_err_msg, wallet.fundrawtransaction, rawtx)
wallet.unloadwallet()


if __name__ == '__main__':
RawTransactionsTest().main()
Loading