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
[wallet] fee fixes: always create change, adjust value, and p… #10333
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2501,6 +2501,13 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT | |
wtxNew.fTimeReceivedIsTxTime = true; | ||
wtxNew.BindWallet(this); | ||
CMutableTransaction txNew; | ||
// This transaction is used to track the best transaction that has change | ||
// falling between uneconomical dust and MIN_FINAL_CHANGE. If the wallet | ||
// runs out of funds trying to find a transaction that has pruneable | ||
// change dust or change larger than MIN_FINAL_CHANGE, it will | ||
// return this transaction. | ||
CMutableTransaction tx_cached; | ||
bool have_cached_txn = false; | ||
|
||
// Discourage fee sniping. | ||
// | ||
|
@@ -2592,73 +2599,69 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT | |
setCoins.clear(); | ||
if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coinControl)) | ||
{ | ||
// We previously succeeded with smaller change we can keep | ||
if (have_cached_txn) { | ||
txNew = tx_cached; | ||
break; | ||
} | ||
strFailReason = _("Insufficient funds"); | ||
return false; | ||
} | ||
|
||
const CAmount nChange = nValueIn - nValueToSelect; | ||
if (nChange > 0) | ||
{ | ||
// Fill a vout to ourself | ||
// TODO: pass in scriptChange instead of reservekey so | ||
// change transaction isn't always pay-to-bitcoin-address | ||
CScript scriptChange; | ||
assert(nChange >= 0); | ||
|
||
// coin control: send change to custom address | ||
if (coinControl && !boost::get<CNoDestination>(&coinControl->destChange)) | ||
scriptChange = GetScriptForDestination(coinControl->destChange); | ||
// Fill a vout to ourself, may be removed later on final fee adjustment | ||
// TODO: pass in scriptChange instead of reservekey so | ||
// change transaction isn't always pay-to-bitcoin-address | ||
CScript scriptChange; | ||
|
||
// no coin control: send change to newly generated address | ||
else | ||
// coin control: send change to custom address | ||
if (coinControl && !boost::get<CNoDestination>(&coinControl->destChange)) { | ||
scriptChange = GetScriptForDestination(coinControl->destChange); | ||
} | ||
// no coin control: send change to newly generated address | ||
else | ||
{ | ||
// Note: We use a new key here to keep it from being obvious which side is the change. | ||
// The drawback is that by not reusing a previous key, the change may be lost if a | ||
// backup is restored, if the backup doesn't have the new private key for the change. | ||
// If we reused the old key, it would be possible to add code to look for and | ||
// rediscover unknown transactions that were written with keys of ours to recover | ||
// post-backup change. | ||
|
||
// Reserve a new key pair from key pool | ||
CPubKey vchPubKey; | ||
bool ret; | ||
ret = reservekey.GetReservedKey(vchPubKey, true); | ||
if (!ret) | ||
{ | ||
// Note: We use a new key here to keep it from being obvious which side is the change. | ||
// The drawback is that by not reusing a previous key, the change may be lost if a | ||
// backup is restored, if the backup doesn't have the new private key for the change. | ||
// If we reused the old key, it would be possible to add code to look for and | ||
// rediscover unknown transactions that were written with keys of ours to recover | ||
// post-backup change. | ||
|
||
// Reserve a new key pair from key pool | ||
CPubKey vchPubKey; | ||
bool ret; | ||
ret = reservekey.GetReservedKey(vchPubKey, true); | ||
if (!ret) | ||
{ | ||
strFailReason = _("Keypool ran out, please call keypoolrefill first"); | ||
return false; | ||
} | ||
|
||
scriptChange = GetScriptForDestination(vchPubKey.GetID()); | ||
strFailReason = _("Keypool ran out, please call keypoolrefill first"); | ||
return false; | ||
} | ||
|
||
CTxOut newTxOut(nChange, scriptChange); | ||
scriptChange = GetScriptForDestination(vchPubKey.GetID()); | ||
} | ||
|
||
// Never create dust outputs; if we would, just | ||
// add the dust to the fee. | ||
if (IsDust(newTxOut, ::dustRelayFee)) | ||
{ | ||
nChangePosInOut = -1; | ||
nFeeRet += nChange; | ||
reservekey.ReturnKey(); | ||
} | ||
else | ||
{ | ||
if (nChangePosInOut == -1) | ||
{ | ||
// Insert change txn at random position: | ||
nChangePosInOut = GetRandInt(txNew.vout.size()+1); | ||
} | ||
else if ((unsigned int)nChangePosInOut > txNew.vout.size()) | ||
{ | ||
strFailReason = _("Change index out of range"); | ||
return false; | ||
} | ||
CTxOut newTxOut(nChange, scriptChange); | ||
|
||
std::vector<CTxOut>::iterator position = txNew.vout.begin()+nChangePosInOut; | ||
txNew.vout.insert(position, newTxOut); | ||
} | ||
// TODO move this section near fee-adjustment area. | ||
if (nChangePosInOut == -1) | ||
{ | ||
// Insert change txn at random position: | ||
nChangePosInOut = GetRandInt(txNew.vout.size()+1); | ||
} | ||
else if ((unsigned int)nChangePosInOut > txNew.vout.size()) | ||
{ | ||
strFailReason = _("Change index out of range"); | ||
return false; | ||
} | ||
|
||
std::vector<CTxOut>::iterator position = txNew.vout.begin()+nChangePosInOut; | ||
// We don't want to add a change output for 0-value with subtractFeeFromAmount | ||
if (nSubtractFeeFromAmount == 0 || nChange > 0) { | ||
txNew.vout.insert(position, newTxOut); | ||
} else { | ||
reservekey.ReturnKey(); | ||
nChangePosInOut = -1; | ||
} | ||
|
||
|
@@ -2709,41 +2712,55 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT | |
return false; | ||
} | ||
|
||
if (nFeeRet >= nFeeNeeded) { | ||
// Reduce fee to only the needed amount if we have change | ||
// output to increase. This prevents potential overpayment | ||
// in fees if the coins selected to meet nFeeNeeded result | ||
// in a transaction that requires less fee than the prior | ||
// iteration. | ||
// TODO: The case where nSubtractFeeFromAmount > 0 remains | ||
// to be addressed because it requires returning the fee to | ||
// the payees and not the change output. | ||
// TODO: The case where there is no change output remains | ||
// to be addressed so we avoid creating too small an output. | ||
if (nFeeRet > nFeeNeeded && nChangePosInOut != -1 && nSubtractFeeFromAmount == 0) { | ||
CAmount extraFeePaid = nFeeRet - nFeeNeeded; | ||
std::vector<CTxOut>::iterator change_position = txNew.vout.begin()+nChangePosInOut; | ||
change_position->nValue += extraFeePaid; | ||
nFeeRet -= extraFeePaid; | ||
// Modify fee to the needed amount from the change | ||
// output, or recipients in the case of subtractFeeFromAmount. | ||
// This prevents potential overpayment in fees if | ||
// the coins selected to meet nFeeNeeded result | ||
// in a transaction that requires less fee than the prior | ||
// iteration. | ||
CAmount excessFee = nFeeRet - nFeeNeeded; | ||
|
||
if (nSubtractFeeFromAmount == 0) { | ||
// Take fee excess | ||
txNew.vout[nChangePosInOut].nValue += excessFee;; | ||
nFeeRet -= excessFee; | ||
|
||
// Negative change value means include more fee and try again. | ||
if (txNew.vout[nChangePosInOut].nValue < 0) { | ||
reservekey.ReturnKey(); | ||
nFeeRet = nFeeNeeded; | ||
continue; | ||
} | ||
break; // Done, enough fee included. | ||
// Drop change if dust | ||
// TODO replace with economical change calc | ||
else if (IsDust(txNew.vout[nChangePosInOut], ::dustRelayFee)) { | ||
nFeeRet += txNew.vout[nChangePosInOut].nValue; | ||
txNew.vout.erase(txNew.vout.begin() + nChangePosInOut); | ||
nChangePosInOut = -1; | ||
reservekey.ReturnKey(); | ||
// If larger than dust, but still small, increase fee target and try again | ||
} else if (txNew.vout[nChangePosInOut].nValue < MIN_FINAL_CHANGE) { | ||
// Save this transaction, use if we cannot get large-enough change | ||
if (!have_cached_txn) { | ||
tx_cached = txNew; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
have_cached_txn = true; | ||
} | ||
reservekey.ReturnKey(); | ||
// Excess fee will be given to change | ||
nFeeRet = nFeeNeeded + (MIN_FINAL_CHANGE/2); | ||
continue; | ||
} | ||
break; | ||
} | ||
|
||
// Try to reduce change to include necessary fee | ||
if (nChangePosInOut != -1 && nSubtractFeeFromAmount == 0) { | ||
CAmount additionalFeeNeeded = nFeeNeeded - nFeeRet; | ||
std::vector<CTxOut>::iterator change_position = txNew.vout.begin()+nChangePosInOut; | ||
// Only reduce change if remaining amount is still a large enough output. | ||
if (change_position->nValue >= MIN_FINAL_CHANGE + additionalFeeNeeded) { | ||
change_position->nValue -= additionalFeeNeeded; | ||
nFeeRet += additionalFeeNeeded; | ||
break; // Done, able to increase fee from change | ||
// TODO: The case where nSubtractFeeFromAmount > 0 remains | ||
// to be addressed because it requires returning the fee to | ||
// the payees and not the change output. | ||
else { | ||
if (excessFee >= 0) { | ||
break; | ||
} | ||
nFeeRet = nFeeNeeded; | ||
} | ||
|
||
// Include more fee and try again. | ||
nFeeRet = nFeeNeeded; | ||
continue; | ||
} | ||
} | ||
|
||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how this case matters. Wouldn't
nChangePosInOut
never be set to an out of range index?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caller may request a too-high change position. This is also a simple code move, not going to change this logic.