diff options
Diffstat (limited to 'src/wallet/spend.cpp')
-rw-r--r-- | src/wallet/spend.cpp | 164 |
1 files changed, 96 insertions, 68 deletions
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index ece5a5c5b7..e97e658f38 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -117,8 +117,9 @@ static std::optional<int64_t> GetSignedTxinWeight(const CWallet* wallet, const C const bool can_grind_r) { // If weight was provided, use that. - if (coin_control && coin_control->HasInputWeight(txin.prevout)) { - return coin_control->GetInputWeight(txin.prevout); + std::optional<int64_t> weight; + if (coin_control && (weight = coin_control->GetInputWeight(txin.prevout))) { + return weight.value(); } // Otherwise, use the maximum satisfaction size provided by the descriptor. @@ -261,7 +262,10 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const const bool can_grind_r = wallet.CanGrindR(); std::map<COutPoint, CAmount> map_of_bump_fees = wallet.chain().calculateIndividualBumpFees(coin_control.ListSelected(), coin_selection_params.m_effective_feerate); for (const COutPoint& outpoint : coin_control.ListSelected()) { - int input_bytes = -1; + int64_t input_bytes = coin_control.GetInputWeight(outpoint).value_or(-1); + if (input_bytes != -1) { + input_bytes = GetVirtualTransactionSize(input_bytes, 0, 0); + } CTxOut txout; if (auto ptr_wtx = wallet.GetWalletTx(outpoint.hash)) { // Clearly invalid input, fail @@ -269,7 +273,9 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const return util::Error{strprintf(_("Invalid pre-selected input %s"), outpoint.ToString())}; } txout = ptr_wtx->tx->vout.at(outpoint.n); - input_bytes = CalculateMaximumSignedInputSize(txout, &wallet, &coin_control); + if (input_bytes == -1) { + input_bytes = CalculateMaximumSignedInputSize(txout, &wallet, &coin_control); + } } else { // The input is external. We did not find the tx in mapWallet. const auto out{coin_control.GetExternalOutput(outpoint)}; @@ -284,11 +290,6 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const input_bytes = CalculateMaximumSignedInputSize(txout, outpoint, &coin_control.m_external_provider, can_grind_r, &coin_control); } - // If available, override calculated size with coin control specified size - if (coin_control.HasInputWeight(outpoint)) { - input_bytes = GetVirtualTransactionSize(coin_control.GetInputWeight(outpoint), 0, 0); - } - if (input_bytes == -1) { return util::Error{strprintf(_("Not solvable pre-selected input %s"), outpoint.ToString())}; // Not solvable, can't estimate size for fee } @@ -967,18 +968,19 @@ static void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng static util::Result<CreatedTransactionResult> CreateTransactionInternal( CWallet& wallet, const std::vector<CRecipient>& vecSend, - int change_pos, + std::optional<unsigned int> change_pos, const CCoinControl& coin_control, bool sign) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) { AssertLockHeld(wallet.cs_wallet); - // out variables, to be packed into returned result structure - int nChangePosInOut = change_pos; - FastRandomContext rng_fast; CMutableTransaction txNew; // The resulting transaction that we make + if (coin_control.m_version) { + txNew.nVersion = coin_control.m_version.value(); + } + CoinSelectionParams coin_selection_params{rng_fast}; // Parameters for coin selection, init with dummy coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends; coin_selection_params.m_include_unsafe_inputs = coin_control.m_include_unsafe_inputs; @@ -1130,20 +1132,39 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal( const CAmount change_amount = result.GetChange(coin_selection_params.min_viable_change, coin_selection_params.m_change_fee); if (change_amount > 0) { CTxOut newTxOut(change_amount, scriptChange); - if (nChangePosInOut == -1) { + if (!change_pos) { // Insert change txn at random position: - nChangePosInOut = rng_fast.randrange(txNew.vout.size() + 1); - } else if ((unsigned int)nChangePosInOut > txNew.vout.size()) { + change_pos = rng_fast.randrange(txNew.vout.size() + 1); + } else if ((unsigned int)*change_pos > txNew.vout.size()) { return util::Error{_("Transaction change output index out of range")}; } - txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut); + txNew.vout.insert(txNew.vout.begin() + *change_pos, newTxOut); } else { - nChangePosInOut = -1; + change_pos = std::nullopt; } // Shuffle selected coins and fill in final vin std::vector<std::shared_ptr<COutput>> selected_coins = result.GetShuffledInputVector(); + if (coin_control.HasSelected() && coin_control.HasSelectedOrder()) { + // When there are preselected inputs, we need to move them to be the first UTXOs + // and have them be in the order selected. We can use stable_sort for this, where we + // compare with the positions stored in coin_control. The COutputs that have positions + // will be placed before those that don't, and those positions will be in order. + std::stable_sort(selected_coins.begin(), selected_coins.end(), + [&coin_control](const std::shared_ptr<COutput>& a, const std::shared_ptr<COutput>& b) { + auto a_pos = coin_control.GetSelectionPos(a->outpoint); + auto b_pos = coin_control.GetSelectionPos(b->outpoint); + if (a_pos.has_value() && b_pos.has_value()) { + return a_pos.value() < b_pos.value(); + } else if (a_pos.has_value() && !b_pos.has_value()) { + return true; + } else { + return false; + } + }); + } + // The sequence number is set to non-maxint so that DiscourageFeeSniping // works. // @@ -1152,11 +1173,32 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal( // to avoid conflicting with other possible uses of nSequence, // and in the spirit of "smallest possible change from prior // behavior." - const uint32_t nSequence{coin_control.m_signal_bip125_rbf.value_or(wallet.m_signal_rbf) ? MAX_BIP125_RBF_SEQUENCE : CTxIn::MAX_SEQUENCE_NONFINAL}; + bool use_anti_fee_sniping = true; + const uint32_t default_sequence{coin_control.m_signal_bip125_rbf.value_or(wallet.m_signal_rbf) ? MAX_BIP125_RBF_SEQUENCE : CTxIn::MAX_SEQUENCE_NONFINAL}; for (const auto& coin : selected_coins) { - txNew.vin.emplace_back(coin->outpoint, CScript(), nSequence); + std::optional<uint32_t> sequence = coin_control.GetSequence(coin->outpoint); + if (sequence) { + // If an input has a preset sequence, we can't do anti-fee-sniping + use_anti_fee_sniping = false; + } + txNew.vin.emplace_back(coin->outpoint, CScript{}, sequence.value_or(default_sequence)); + + auto scripts = coin_control.GetScripts(coin->outpoint); + if (scripts.first) { + txNew.vin.back().scriptSig = *scripts.first; + } + if (scripts.second) { + txNew.vin.back().scriptWitness = *scripts.second; + } + } + if (coin_control.m_locktime) { + txNew.nLockTime = coin_control.m_locktime.value(); + // If we have a locktime set, we can't use anti-fee-sniping + use_anti_fee_sniping = false; + } + if (use_anti_fee_sniping) { + DiscourageFeeSniping(txNew, rng_fast, wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight()); } - DiscourageFeeSniping(txNew, rng_fast, wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight()); // Calculate the transaction fee TxSize tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, &coin_control); @@ -1175,8 +1217,8 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal( } // If there is a change output and we overpay the fees then increase the change to match the fee needed - if (nChangePosInOut != -1 && fee_needed < current_fee) { - auto& change = txNew.vout.at(nChangePosInOut); + if (change_pos && fee_needed < current_fee) { + auto& change = txNew.vout.at(*change_pos); change.nValue += current_fee - fee_needed; current_fee = result.GetSelectedValue() - CalculateOutputValue(txNew); if (fee_needed != current_fee) { @@ -1187,11 +1229,11 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal( // Reduce output values for subtractFeeFromAmount if (coin_selection_params.m_subtract_fee_outputs) { CAmount to_reduce = fee_needed - current_fee; - int i = 0; + unsigned int i = 0; bool fFirst = true; for (const auto& recipient : vecSend) { - if (i == nChangePosInOut) { + if (change_pos && i == *change_pos) { ++i; } CTxOut& txout = txNew.vout[i]; @@ -1230,7 +1272,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal( } // Give up if change keypool ran out and change is required - if (scriptChange.empty() && nChangePosInOut != -1) { + if (scriptChange.empty() && change_pos) { return util::Error{error}; } @@ -1272,13 +1314,13 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal( feeCalc.est.fail.start, feeCalc.est.fail.end, (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) > 0.0 ? 100 * feeCalc.est.fail.withinTarget / (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) : 0.0, feeCalc.est.fail.withinTarget, feeCalc.est.fail.totalConfirmed, feeCalc.est.fail.inMempool, feeCalc.est.fail.leftMempool); - return CreatedTransactionResult(tx, current_fee, nChangePosInOut, feeCalc); + return CreatedTransactionResult(tx, current_fee, change_pos, feeCalc); } util::Result<CreatedTransactionResult> CreateTransaction( CWallet& wallet, const std::vector<CRecipient>& vecSend, - int change_pos, + std::optional<unsigned int> change_pos, const CCoinControl& coin_control, bool sign) { @@ -1294,7 +1336,7 @@ util::Result<CreatedTransactionResult> CreateTransaction( auto res = CreateTransactionInternal(wallet, vecSend, change_pos, coin_control, sign); TRACE4(coin_selection, normal_create_tx_internal, wallet.GetName().c_str(), bool(res), - res ? res->fee : 0, res ? res->change_pos : 0); + res ? res->fee : 0, res && res->change_pos.has_value() ? *res->change_pos : 0); if (!res) return res; const auto& txr_ungrouped = *res; // try with avoidpartialspends unless it's enabled already @@ -1304,16 +1346,15 @@ util::Result<CreatedTransactionResult> CreateTransaction( tmp_cc.m_avoid_partial_spends = true; // Reuse the change destination from the first creation attempt to avoid skipping BIP44 indexes - const int ungrouped_change_pos = txr_ungrouped.change_pos; - if (ungrouped_change_pos != -1) { - ExtractDestination(txr_ungrouped.tx->vout[ungrouped_change_pos].scriptPubKey, tmp_cc.destChange); + if (txr_ungrouped.change_pos) { + ExtractDestination(txr_ungrouped.tx->vout[*txr_ungrouped.change_pos].scriptPubKey, tmp_cc.destChange); } auto txr_grouped = CreateTransactionInternal(wallet, vecSend, change_pos, tmp_cc, sign); // if fee of this alternative one is within the range of the max fee, we use this one const bool use_aps{txr_grouped.has_value() ? (txr_grouped->fee <= txr_ungrouped.fee + wallet.m_max_aps_fee) : false}; TRACE5(coin_selection, aps_create_tx_internal, wallet.GetName().c_str(), use_aps, txr_grouped.has_value(), - txr_grouped.has_value() ? txr_grouped->fee : 0, txr_grouped.has_value() ? txr_grouped->change_pos : 0); + txr_grouped.has_value() ? txr_grouped->fee : 0, txr_grouped.has_value() && txr_grouped->change_pos.has_value() ? *txr_grouped->change_pos : 0); if (txr_grouped) { wallet.WalletLogPrintf("Fee non-grouped = %lld, grouped = %lld, using %s\n", txr_ungrouped.fee, txr_grouped->fee, use_aps ? "grouped" : "non-grouped"); @@ -1323,7 +1364,7 @@ util::Result<CreatedTransactionResult> CreateTransaction( return res; } -bool FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, CCoinControl coinControl) +util::Result<CreatedTransactionResult> FundTransaction(CWallet& wallet, const CMutableTransaction& tx, std::optional<unsigned int> change_pos, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, CCoinControl coinControl) { std::vector<CRecipient> vecSend; @@ -1336,6 +1377,12 @@ bool FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& nFeeRet, vecSend.push_back(recipient); } + // Set the user desired locktime + coinControl.m_locktime = tx.nLockTime; + + // Set the user desired version + coinControl.m_version = tx.nVersion; + // Acquire the locks to prevent races to the new locked unspents between the // CreateTransaction call and LockCoin calls (when lockUnspents is true). LOCK(wallet.cs_wallet); @@ -1350,50 +1397,31 @@ bool FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& nFeeRet, for (const CTxIn& txin : tx.vin) { const auto& outPoint = txin.prevout; - if (wallet.IsMine(outPoint)) { - // The input was found in the wallet, so select as internal - coinControl.Select(outPoint); - } else if (coins[outPoint].out.IsNull()) { - error = _("Unable to find UTXO for external input"); - return false; - } else { + PreselectedInput& preset_txin = coinControl.Select(outPoint); + if (!wallet.IsMine(outPoint)) { + if (coins[outPoint].out.IsNull()) { + return util::Error{_("Unable to find UTXO for external input")}; + } + // The input was not in the wallet, but is in the UTXO set, so select as external - coinControl.SelectExternal(outPoint, coins[outPoint].out); + preset_txin.SetTxOut(coins[outPoint].out); } + preset_txin.SetSequence(txin.nSequence); + preset_txin.SetScriptSig(txin.scriptSig); + preset_txin.SetScriptWitness(txin.scriptWitness); } - auto res = CreateTransaction(wallet, vecSend, nChangePosInOut, coinControl, false); + auto res = CreateTransaction(wallet, vecSend, change_pos, coinControl, false); if (!res) { - error = util::ErrorString(res); - return false; - } - const auto& txr = *res; - CTransactionRef tx_new = txr.tx; - nFeeRet = txr.fee; - nChangePosInOut = txr.change_pos; - - if (nChangePosInOut != -1) { - tx.vout.insert(tx.vout.begin() + nChangePosInOut, tx_new->vout[nChangePosInOut]); - } - - // Copy output sizes from new transaction; they may have had the fee - // subtracted from them. - for (unsigned int idx = 0; idx < tx.vout.size(); idx++) { - tx.vout[idx].nValue = tx_new->vout[idx].nValue; + return res; } - // Add new txins while keeping original txin scriptSig/order. - for (const CTxIn& txin : tx_new->vin) { - if (!coinControl.IsSelected(txin.prevout)) { - tx.vin.push_back(txin); - - } - if (lockUnspents) { + if (lockUnspents) { + for (const CTxIn& txin : res->tx->vin) { wallet.LockCoin(txin.prevout); } - } - return true; + return res; } } // namespace wallet |