0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-02-08 10:31:50 -05:00

Merge #16208: wallet: Consume ReserveDestination on successful CreateTransaction

e10e1e8db0 Restrict lifetime of ReserveDestination to CWallet::CreateTransaction (Gregory Sanders)
d9ff862f2d CreateTransaction calls KeepDestination on ReserveDestination before success (Gregory Sanders)

Pull request description:

  The typical usage pattern of `ReserveDestination` is to explicitly `KeepDestination`, or `ReturnDestination` when it's detected it will not be used.

  Implementers such as myself may fail to complete this pattern, and could result in key re-use: https://github.com/bitcoin/bitcoin/pull/15557#discussion_r271956393

  Since ReserveDestination is currently only used directly in the `CreateTransaction`/`CommitTransaction` flow(or fee bumping where it's just used in `CreateTransaction`), I instead make the assumption that if a transaction is returned by `CreateTransaction` it's highly likely that it will be accepted by the caller, and the `ReserveDestination` kept. This simplifies the API as well. There are very few cases where this would not be the case which may result in keys being burned.

  Those failure cases appear to be:
  `CommitTransaction` failing to get the transaction into the mempool
  Belt and suspenders check in `WalletModel::prepareTransaction`

  Alternative to https://github.com/bitcoin/bitcoin/pull/15796

ACKs for top commit:
  achow101:
    ACK e10e1e8db0 Reviewed the diff
  stevenroose:
    utACK e10e1e8db0
  meshcollider:
    utACK e10e1e8db0

Tree-SHA512: 78d047a00f39ab41cfa297052cc1e9c224d5f47d3d2299face650d71827635de077ac33fb4ab9f7dc6fc5a27f4a68415a1bc9ca33a3cb09a78f4f15b2a48411b
This commit is contained in:
MeshCollider 2019-07-17 19:45:14 +12:00
commit 459baa1756
No known key found for this signature in database
GPG key ID: D300116E1C875A3D
6 changed files with 23 additions and 35 deletions

View file

@ -36,7 +36,7 @@ namespace {
class PendingWalletTxImpl : public PendingWalletTx class PendingWalletTxImpl : public PendingWalletTx
{ {
public: public:
explicit PendingWalletTxImpl(CWallet& wallet) : m_wallet(wallet), m_dest(&wallet) {} explicit PendingWalletTxImpl(CWallet& wallet) : m_wallet(wallet) {}
const CTransaction& get() override { return *m_tx; } const CTransaction& get() override { return *m_tx; }
@ -47,7 +47,7 @@ public:
auto locked_chain = m_wallet.chain().lock(); auto locked_chain = m_wallet.chain().lock();
LOCK(m_wallet.cs_wallet); LOCK(m_wallet.cs_wallet);
CValidationState state; CValidationState state;
if (!m_wallet.CommitTransaction(m_tx, std::move(value_map), std::move(order_form), m_dest, state)) { if (!m_wallet.CommitTransaction(m_tx, std::move(value_map), std::move(order_form), state)) {
reject_reason = state.GetRejectReason(); reject_reason = state.GetRejectReason();
return false; return false;
} }
@ -56,7 +56,6 @@ public:
CTransactionRef m_tx; CTransactionRef m_tx;
CWallet& m_wallet; CWallet& m_wallet;
ReserveDestination m_dest;
}; };
//! Construct wallet tx struct. //! Construct wallet tx struct.
@ -238,7 +237,7 @@ public:
auto locked_chain = m_wallet->chain().lock(); auto locked_chain = m_wallet->chain().lock();
LOCK(m_wallet->cs_wallet); LOCK(m_wallet->cs_wallet);
auto pending = MakeUnique<PendingWalletTxImpl>(*m_wallet); auto pending = MakeUnique<PendingWalletTxImpl>(*m_wallet);
if (!m_wallet->CreateTransaction(*locked_chain, recipients, pending->m_tx, pending->m_dest, fee, change_pos, if (!m_wallet->CreateTransaction(*locked_chain, recipients, pending->m_tx, fee, change_pos,
fail_reason, coin_control, sign)) { fail_reason, coin_control, sign)) {
return {}; return {};
} }

View file

@ -272,18 +272,14 @@ Result CreateRateBumpTransaction(CWallet* wallet, const uint256& txid, const CCo
new_coin_control.m_min_depth = 1; new_coin_control.m_min_depth = 1;
CTransactionRef tx_new = MakeTransactionRef(); CTransactionRef tx_new = MakeTransactionRef();
ReserveDestination reservedest(wallet);
CAmount fee_ret; CAmount fee_ret;
int change_pos_in_out = -1; // No requested location for change int change_pos_in_out = -1; // No requested location for change
std::string fail_reason; std::string fail_reason;
if (!wallet->CreateTransaction(*locked_chain, recipients, tx_new, reservedest, fee_ret, change_pos_in_out, fail_reason, new_coin_control, false)) { if (!wallet->CreateTransaction(*locked_chain, recipients, tx_new, fee_ret, change_pos_in_out, fail_reason, new_coin_control, false)) {
errors.push_back("Unable to create transaction: " + fail_reason); errors.push_back("Unable to create transaction: " + fail_reason);
return Result::WALLET_ERROR; return Result::WALLET_ERROR;
} }
// If change key hasn't been ReturnKey'ed by this point, we take it out of keypool
reservedest.KeepDestination();
// Write back new fee if successful // Write back new fee if successful
new_fee = fee_ret; new_fee = fee_ret;
@ -330,9 +326,8 @@ Result CommitTransaction(CWallet* wallet, const uint256& txid, CMutableTransacti
mapValue_t mapValue = oldWtx.mapValue; mapValue_t mapValue = oldWtx.mapValue;
mapValue["replaces_txid"] = oldWtx.GetHash().ToString(); mapValue["replaces_txid"] = oldWtx.GetHash().ToString();
ReserveDestination reservedest(wallet);
CValidationState state; CValidationState state;
if (!wallet->CommitTransaction(tx, std::move(mapValue), oldWtx.vOrderForm, reservedest, state)) { if (!wallet->CommitTransaction(tx, std::move(mapValue), oldWtx.vOrderForm, state)) {
// NOTE: CommitTransaction never returns false, so this should never happen. // NOTE: CommitTransaction never returns false, so this should never happen.
errors.push_back(strprintf("The transaction was rejected: %s", FormatStateMessage(state))); errors.push_back(strprintf("The transaction was rejected: %s", FormatStateMessage(state)));
return Result::WALLET_ERROR; return Result::WALLET_ERROR;

View file

@ -317,7 +317,6 @@ static CTransactionRef SendMoney(interfaces::Chain::Lock& locked_chain, CWallet
CScript scriptPubKey = GetScriptForDestination(address); CScript scriptPubKey = GetScriptForDestination(address);
// Create and send the transaction // Create and send the transaction
ReserveDestination reservedest(pwallet);
CAmount nFeeRequired; CAmount nFeeRequired;
std::string strError; std::string strError;
std::vector<CRecipient> vecSend; std::vector<CRecipient> vecSend;
@ -325,13 +324,13 @@ static CTransactionRef SendMoney(interfaces::Chain::Lock& locked_chain, CWallet
CRecipient recipient = {scriptPubKey, nValue, fSubtractFeeFromAmount}; CRecipient recipient = {scriptPubKey, nValue, fSubtractFeeFromAmount};
vecSend.push_back(recipient); vecSend.push_back(recipient);
CTransactionRef tx; CTransactionRef tx;
if (!pwallet->CreateTransaction(locked_chain, vecSend, tx, reservedest, nFeeRequired, nChangePosRet, strError, coin_control)) { if (!pwallet->CreateTransaction(locked_chain, vecSend, tx, nFeeRequired, nChangePosRet, strError, coin_control)) {
if (!fSubtractFeeFromAmount && nValue + nFeeRequired > curBalance) if (!fSubtractFeeFromAmount && nValue + nFeeRequired > curBalance)
strError = strprintf("Error: This transaction requires a transaction fee of at least %s", FormatMoney(nFeeRequired)); strError = strprintf("Error: This transaction requires a transaction fee of at least %s", FormatMoney(nFeeRequired));
throw JSONRPCError(RPC_WALLET_ERROR, strError); throw JSONRPCError(RPC_WALLET_ERROR, strError);
} }
CValidationState state; CValidationState state;
if (!pwallet->CommitTransaction(tx, std::move(mapValue), {} /* orderForm */, reservedest, state)) { if (!pwallet->CommitTransaction(tx, std::move(mapValue), {} /* orderForm */, state)) {
strError = strprintf("Error: The transaction was rejected! Reason given: %s", FormatStateMessage(state)); strError = strprintf("Error: The transaction was rejected! Reason given: %s", FormatStateMessage(state));
throw JSONRPCError(RPC_WALLET_ERROR, strError); throw JSONRPCError(RPC_WALLET_ERROR, strError);
} }
@ -915,16 +914,15 @@ static UniValue sendmany(const JSONRPCRequest& request)
std::shuffle(vecSend.begin(), vecSend.end(), FastRandomContext()); std::shuffle(vecSend.begin(), vecSend.end(), FastRandomContext());
// Send // Send
ReserveDestination changedest(pwallet);
CAmount nFeeRequired = 0; CAmount nFeeRequired = 0;
int nChangePosRet = -1; int nChangePosRet = -1;
std::string strFailReason; std::string strFailReason;
CTransactionRef tx; CTransactionRef tx;
bool fCreated = pwallet->CreateTransaction(*locked_chain, vecSend, tx, changedest, nFeeRequired, nChangePosRet, strFailReason, coin_control); bool fCreated = pwallet->CreateTransaction(*locked_chain, vecSend, tx, nFeeRequired, nChangePosRet, strFailReason, coin_control);
if (!fCreated) if (!fCreated)
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strFailReason); throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strFailReason);
CValidationState state; CValidationState state;
if (!pwallet->CommitTransaction(tx, std::move(mapValue), {} /* orderForm */, changedest, state)) { if (!pwallet->CommitTransaction(tx, std::move(mapValue), {} /* orderForm */, state)) {
strFailReason = strprintf("Transaction commit failed:: %s", FormatStateMessage(state)); strFailReason = strprintf("Transaction commit failed:: %s", FormatStateMessage(state));
throw JSONRPCError(RPC_WALLET_ERROR, strFailReason); throw JSONRPCError(RPC_WALLET_ERROR, strFailReason);
} }

View file

@ -361,17 +361,16 @@ public:
CWalletTx& AddTx(CRecipient recipient) CWalletTx& AddTx(CRecipient recipient)
{ {
CTransactionRef tx; CTransactionRef tx;
ReserveDestination reservedest(wallet.get());
CAmount fee; CAmount fee;
int changePos = -1; int changePos = -1;
std::string error; std::string error;
CCoinControl dummy; CCoinControl dummy;
{ {
auto locked_chain = m_chain->lock(); auto locked_chain = m_chain->lock();
BOOST_CHECK(wallet->CreateTransaction(*locked_chain, {recipient}, tx, reservedest, fee, changePos, error, dummy)); BOOST_CHECK(wallet->CreateTransaction(*locked_chain, {recipient}, tx, fee, changePos, error, dummy));
} }
CValidationState state; CValidationState state;
BOOST_CHECK(wallet->CommitTransaction(tx, {}, {}, reservedest, state)); BOOST_CHECK(wallet->CommitTransaction(tx, {}, {}, state));
CMutableTransaction blocktx; CMutableTransaction blocktx;
{ {
LOCK(wallet->cs_wallet); LOCK(wallet->cs_wallet);

View file

@ -2779,17 +2779,13 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nC
auto locked_chain = chain().lock(); auto locked_chain = chain().lock();
LOCK(cs_wallet); LOCK(cs_wallet);
ReserveDestination reservedest(this);
CTransactionRef tx_new; CTransactionRef tx_new;
if (!CreateTransaction(*locked_chain, vecSend, tx_new, reservedest, nFeeRet, nChangePosInOut, strFailReason, coinControl, false)) { if (!CreateTransaction(*locked_chain, vecSend, tx_new, nFeeRet, nChangePosInOut, strFailReason, coinControl, false)) {
return false; return false;
} }
if (nChangePosInOut != -1) { if (nChangePosInOut != -1) {
tx.vout.insert(tx.vout.begin() + nChangePosInOut, tx_new->vout[nChangePosInOut]); tx.vout.insert(tx.vout.begin() + nChangePosInOut, tx_new->vout[nChangePosInOut]);
// We don't have the normal Create/Commit cycle, and don't want to risk
// reusing change, so just remove the key from the keypool here.
reservedest.KeepDestination();
} }
// Copy output sizes from new transaction; they may have had the fee // Copy output sizes from new transaction; they may have had the fee
@ -2900,10 +2896,11 @@ OutputType CWallet::TransactionChangeType(OutputType change_type, const std::vec
return m_default_address_type; return m_default_address_type;
} }
bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std::vector<CRecipient>& vecSend, CTransactionRef& tx, ReserveDestination& reservedest, CAmount& nFeeRet, bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std::vector<CRecipient>& vecSend, CTransactionRef& tx, CAmount& nFeeRet,
int& nChangePosInOut, std::string& strFailReason, const CCoinControl& coin_control, bool sign) int& nChangePosInOut, std::string& strFailReason, const CCoinControl& coin_control, bool sign)
{ {
CAmount nValue = 0; CAmount nValue = 0;
ReserveDestination reservedest(this);
int nChangePosRequest = nChangePosInOut; int nChangePosRequest = nChangePosInOut;
unsigned int nSubtractFeeFromAmount = 0; unsigned int nSubtractFeeFromAmount = 0;
for (const auto& recipient : vecSend) for (const auto& recipient : vecSend)
@ -3183,8 +3180,6 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std
} }
} }
if (nChangePosInOut == -1) reservedest.ReturnDestination(); // Return any reserved address if we don't have change
// Shuffle selected coins and fill in final vin // Shuffle selected coins and fill in final vin
txNew.vin.clear(); txNew.vin.clear();
std::vector<CInputCoin> selected_coins(setCoins.begin(), setCoins.end()); std::vector<CInputCoin> selected_coins(setCoins.begin(), setCoins.end());
@ -3247,6 +3242,10 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std
} }
} }
// Before we return success, we assume any change key will be used to prevent
// accidental re-use.
reservedest.KeepDestination();
WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Needed:%d Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n", WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Needed:%d Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n",
nFeeRet, nBytes, nFeeNeeded, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay, nFeeRet, nBytes, nFeeNeeded, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay,
feeCalc.est.pass.start, feeCalc.est.pass.end, feeCalc.est.pass.start, feeCalc.est.pass.end,
@ -3261,7 +3260,7 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std
/** /**
* Call after CreateTransaction unless you want to abort * Call after CreateTransaction unless you want to abort
*/ */
bool CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::vector<std::pair<std::string, std::string>> orderForm, ReserveDestination& reservedest, CValidationState& state) bool CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::vector<std::pair<std::string, std::string>> orderForm, CValidationState& state)
{ {
{ {
auto locked_chain = chain().lock(); auto locked_chain = chain().lock();
@ -3275,8 +3274,6 @@ bool CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::ve
WalletLogPrintf("CommitTransaction:\n%s", wtxNew.tx->ToString()); /* Continued */ WalletLogPrintf("CommitTransaction:\n%s", wtxNew.tx->ToString()); /* Continued */
{ {
// Take key pair from key pool so it won't be used again
reservedest.KeepDestination();
// Add tx to wallet, because if it has change it's also ours, // Add tx to wallet, because if it has change it's also ours,
// otherwise just for transaction history. // otherwise just for transaction history.

View file

@ -266,8 +266,8 @@ public:
/** A wrapper to reserve an address from a wallet /** A wrapper to reserve an address from a wallet
* *
* ReserveDestination is used to reserve an address. It is passed around * ReserveDestination is used to reserve an address.
* during the CreateTransaction/CommitTransaction procedure. * It is currently only used inside of CreateTransaction.
* *
* Instantiating a ReserveDestination does not reserve an address. To do so, * Instantiating a ReserveDestination does not reserve an address. To do so,
* GetReservedDestination() needs to be called on the object. Once an address has been * GetReservedDestination() needs to be called on the object. Once an address has been
@ -1137,9 +1137,9 @@ public:
* selected by SelectCoins(); Also create the change output, when needed * selected by SelectCoins(); Also create the change output, when needed
* @note passing nChangePosInOut as -1 will result in setting a random position * @note passing nChangePosInOut as -1 will result in setting a random position
*/ */
bool CreateTransaction(interfaces::Chain::Lock& locked_chain, const std::vector<CRecipient>& vecSend, CTransactionRef& tx, ReserveDestination& reservedest, CAmount& nFeeRet, int& nChangePosInOut, bool CreateTransaction(interfaces::Chain::Lock& locked_chain, const std::vector<CRecipient>& vecSend, CTransactionRef& tx, CAmount& nFeeRet, int& nChangePosInOut,
std::string& strFailReason, const CCoinControl& coin_control, bool sign = true); std::string& strFailReason, const CCoinControl& coin_control, bool sign = true);
bool CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::vector<std::pair<std::string, std::string>> orderForm, ReserveDestination& reservedest, CValidationState& state); bool CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::vector<std::pair<std::string, std::string>> orderForm, CValidationState& state);
bool DummySignTx(CMutableTransaction &txNew, const std::set<CTxOut> &txouts, bool use_max_sig = false) const bool DummySignTx(CMutableTransaction &txNew, const std::set<CTxOut> &txouts, bool use_max_sig = false) const
{ {