This is an alternative to #10333
See commit messages.
The first commit is mostly code move, it just moves the change creation code out of the loop. @instagibbs
This is an alternative to #10333
See commit messages.
The first commit is mostly code move, it just moves the change creation code out of the loop. @instagibbs
2750@@ -2744,8 +2751,18 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
2751 // TODO: The case where nSubtractFeeFromAmount > 0 remains
2752 // to be addressed because it requires returning the fee to
2753 // the payees and not the change output.
2754- // TODO: The case where there is no change output remains
2755- // to be addressed so we avoid creating too small an output.
2756+ CAmount max_excess_fee = GetMinimumFee(change_prototype_size, currentConfirmationTarget, ::mempool, ::feeEstimator, nullptr) +
2757+ GetDustThreshold(change_prototype_txout, ::dustRelayFee);
2758+ // If we have no change and a big enough excess fee, then
please move first explanation a few lines before for the “increase change” to appropriate spot
I think order of the comments does makes sense. First comment describes the normal case, second comment describes unhandled special case, and third comment describes handled special case accompanied by implementation for that case.
I might move the code for the normal case above the code & comments for the two special cases, though this would increase the size of the diff. Just a thought, though.
2770@@ -2754,6 +2771,11 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
2771 }
2772 break; // Done, enough fee included.
2773 }
2774+ else if (!pick_new_inputs) {
2775+ // This shouldn't happen
nit: slightly longer explanation for reader?
“This shouldn’t happen, we should have had enough excess fee to pay for the newly created change output.”
With fix on the nFeeRet, a very slight increase in fee between estimation calls(can that happen?) will result in this triggering.
2772@@ -2774,6 +2773,8 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
2773 }
2774 }
2775
2776+ if (nChangePosInOut == -1) reserveKey.ReturnKey(); // Return any reserved key if we don't have change
2760+ // new inputs. We now know we only need the smaller fee
2761+ // (because of reduced tx size) and so we should add a
2762+ // change input. Only try this once.
2763+ if (nFeeRet > nFeeNeeded + max_excess_fee && nChangePosInOut == -1 && nSubtractFeeFromAmount == 0 && pick_new_inputs) {
2764+ pick_new_inputs = false;
2765+ nFeeRet = nFeeNeeded;
this needs to be nFeeRet = nFeeNeeded + GetMinimumFee(change_prototype_size, currentConfirmationTarget, ::mempool, ::feeEstimator, nullptr) ;
at a minimum, right?
nFeeNeeded at this point will not pay for the new change output.
imo this is superior in its simplicity and targeting the exact situation better over #10333 .
A bit worried about the failure case triggering anytime fee estimations change. I know you don’t want to in this PR, but pulling out the estimation and getting a feerate that is reapplied in the main loop seems worth it, is easy to review, and will have to be done anyways for effective value work.
reservekey.ReturnKey()
?
utACK.
IMO going for one last iteration and to have one more flag is a clear sign that a more thoughtful refactor should be done. Not that I’m against this change, but reading this code should be plain and easy.
@promag New variables are supposed to be snake_case, did I miss one? If there are any other changes, I can fix the braces, but I didn’t touch the moved code.
As for calling ReturnKey, I suppose that does make sense, but the existing code did not do that, and I haven’t materially changed that behavior. I’m happy to add it though if I should? (Clarification: My only hesitation is that it looks to me like it’s fine to call ReturnKey even if you haven’t reserved one, but I’m just not that familiar with the code or it’s design, so was hesitant to put in extra calls to ReturnKey where one might not have actually been reserved)
2799@@ -2773,7 +2800,7 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
2800 }
2801 }
2802
2803- if (nChangePosInOut == -1) reserveKey.ReturnKey(); // Return any reserved key if we don't have change
2804+ if (nChangePosInOut == -1) reservekey.ReturnKey(); // Return any reserved key if we don't have change
In commit “Fix rare edge case of paying too many fees”
This needs to be moved to the previous commit for it to compile.
2761+
2762+ // If we have no change and a big enough excess fee, then
2763+ // try to construct transaction again only without picking
2764+ // new inputs. We now know we only need the smaller fee
2765+ // (because of reduced tx size) and so we should add a
2766+ // change input. Only try this once.
Wow, this code is ridiculously complicated. Pretty clean fix, though. Would be nice if we could mock SelectCoins
and easily write unit tests for these cases.
utACK d64a4b94ea809179e8cc9a92be75036bc8bd4fbf
Fixed commit history and fixed comment nit
0git diff d64a4b9 533b3f0
1
2- // change input. Only try this once.
3+ // change output. Only try this once.
2774@@ -2754,6 +2775,12 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
2775 }
2776 break; // Done, enough fee included.
2777 }
2778+ else if (!pick_new_inputs) {
}
This does not affect behavior but allows us to have access to an output to
scriptChange even if we currently do not have change in the transaction.
Due to the iterative process of selecting new coins in each loop a new fee is
calculated that needs to be met each time. In the typical case if the most
recent iteration of the loop produced a much smaller transaction and we have now
gathered inputs with too many fees, we can just reduce the change. However in
the case where there is no change output, it is possible to end up with a
transaction which drastically overpays fees. This commit addresses that case,
by creating a change output if the overpayment is large enough to support it,
this is accomplished by rerunning the transaction creation loop without
selecting new coins.
Thanks to instagibbs for working on this as well